Compare commits

..

251 Commits

Author SHA1 Message Date
Andrew Pareles 2f80c653d5 certs 2025-05-13 19:52:26 -07:00
Andrew Pareles b35dfdf475 trigger resize 2025-05-13 18:29:04 -07:00
Andrew Pareles 609f4c9cbf UI 2025-05-13 17:27:46 -07:00
Andrew Pareles 6df3b588df fix anthropic empty err message 2025-05-13 17:25:29 -07:00
Andrew Pareles f4c1321f25 rm thinking from 2 flash 2025-05-12 20:08:54 -07:00
Andrew Pareles b54dd7fedc 1.3.7 / 0031 2025-05-12 19:53:58 -07:00
Andrew Pareles 3241178238 Merge remote-tracking branch 'origin/main' into model-selection 2025-05-12 19:50:35 -07:00
Andrew Pareles 68bb3f6962 fix azure? 2025-05-12 19:48:50 -07:00
Andrew Pareles 2e64f5a24b overrides link update 2025-05-12 19:46:18 -07:00
Andrew Pareles 2e5ecb291d comment 2025-05-12 19:44:42 -07:00
Andrew Pareles f1e24c9b4c Merge pull request #545 from HunterEvangelista/510-openai-additional-json
added optional json config for VoidStaticModelInfo
2025-05-12 19:42:22 -07:00
Andrew Pareles f65e4f51a2 misc 2025-05-12 19:40:39 -07:00
Andrew Pareles f1a5d20150 Merge pull request #505 from homanp/feat/apply-to-terminal
feat: add support for applying `shellscript` and `bash` directly to terminal
2025-05-12 19:32:24 -07:00
Andrew Pareles 28dcdff189 isdisabled fix 2025-05-12 19:31:05 -07:00
Andrew Pareles 67ed52415e apply terminal? 2025-05-12 19:29:45 -07:00
Andrew Pareles 3772cf98ea delete 2025-05-12 18:47:40 -07:00
Andrew Pareles 1c6edf7023 generalize 2025-05-12 18:45:33 -07:00
Andrew Pareles 6b6528c3fb Merge pull request #507 from steaks/explorer-view-copy-prompt
Add Void: Copy Prompt as menu item to the ExplorerContext window
2025-05-12 18:30:21 -07:00
Andrew Pareles f98894ad77 format 2025-05-12 18:22:59 -07:00
Andrew Pareles f8745d8b40 Merge remote-tracking branch 'origin/main' into pr/steaks/507 2025-05-12 18:20:55 -07:00
Andrew Pareles 7066b4285b file and folder copy uniform 2025-05-12 18:20:44 -07:00
Andrew Pareles 1eee07e697 Merge pull request #548 from ujjwaljainnn/ujjwaljainnn-add-context-on-add-to-chat
fix: preserve selection when adding to chat via CTRL+L with closed panel
2025-05-12 16:50:04 -07:00
Andrew Pareles 45f7e4b157 minor updates 2025-05-12 16:48:27 -07:00
Andrew Pareles 1619a3c19b Merge pull request #559 from vrtnis/mistral-medium-capabilities
feat(models): update modelCapabilities with mistral-medium entry
2025-05-12 14:37:40 -07:00
vrtnis d668348244 feat(models): update modelCapabilities with mistral-medium entry 2025-05-12 20:58:55 +00:00
Andrew Pareles 3fa680e8ba identify more shells 2025-05-12 12:09:55 -07:00
Andrew Pareles e8b7c9c470 always create terminal when apply 2025-05-12 11:56:08 -07:00
Andrew Pareles a0eb7d6192 Merge remote-tracking branch 'origin/main' into pr/homanp/505 2025-05-12 11:52:22 -07:00
Andrew Pareles 5c4785d691 Merge remote-tracking branch 'origin/model-selection' into pr/homanp/505 2025-05-12 11:50:36 -07:00
Andrew Pareles 1de90cc39d use 1st terminal id 2025-05-12 11:50:25 -07:00
Andrew Pareles d09a41acd3 Merge pull request #556 from voideditor/model-selection
delete
2025-05-12 11:03:17 -07:00
Andrew Pareles 24102fc2b2 log2 2025-05-12 11:00:05 -07:00
Andrew Pareles c2b65eb16f log 2025-05-12 10:59:53 -07:00
Andrew Pareles a4046bc5a5 update so edit extensions.json 2025-05-12 10:58:18 -07:00
Andrew Pareles 6f73804e94 update 2025-05-12 10:50:28 -07:00
Andrew Pareles 76155beaad Merge pull request #555 from voideditor/model-selection
extensions warning fix by deleting some extensions once
2025-05-12 10:45:08 -07:00
Andrew Pareles 53c68b1267 bump 2025-05-12 10:44:43 -07:00
Andrew Pareles afbc1b40bf extensions warning fix by deleting some extensions once 2025-05-12 10:42:40 -07:00
Ismail Pelaseyed 9a905f6cb2 add support for stopping the terminal command 2025-05-12 09:06:22 +02:00
Ujjwal Jain dd42e40a8d fix: preserve selection when adding to chat via CTRL+L with closed panel 2025-05-11 22:01:04 -04:00
Hunter Evangelista 970fd7ecbb moved payload to sdk initialization 2025-05-11 17:10:07 -07:00
Hunter Evangelista 50710143eb Merge branch 'main' into 510-openai-additional-json 2025-05-11 16:06:11 -07:00
Hunter Evangelista d6295e5d65 added optional json config for VoidStaticModelInfo 2025-05-11 16:02:17 -07:00
Andrew Pareles 053a022ef8 Merge pull request #538 from voideditor/model-selection
1.3.5
2025-05-10 22:28:33 -07:00
Andrew Pareles 3b963d8717 1.3.5 2025-05-10 22:28:15 -07:00
Andrew Pareles e8540e4668 Merge pull request #537 from voideditor/model-selection
Cmd L better
2025-05-10 22:27:36 -07:00
Andrew Pareles 39f2f24b42 1.3.4 2025-05-10 22:07:26 -07:00
Andrew Pareles e852874be7 cmd L to add file 2025-05-10 22:05:50 -07:00
Andrew Pareles 6d07f9db14 Reasoning improvements + Gemini (Merge pull request #521 from voideditor/model-selection)
Gemini reasoning improvements + link URI improvements
2025-05-10 18:10:34 -07:00
Andrew Pareles 2934f9a3c5 += -> = 2025-05-10 18:10:00 -07:00
Andrew Pareles 7748dc4f2d misc fixes + (empty message) improvements 2025-05-10 18:06:58 -07:00
Andrew Pareles 8b45aea8a8 fix link + add gemini tool id if empty 2025-05-10 16:28:54 -07:00
Andrew Pareles cab8dd64b6 rm comments 2025-05-10 16:13:36 -07:00
Andrew Pareles 19782c80df voidRelease 2025-05-10 16:09:06 -07:00
Andrew Pareles 926db45f0a 33 2025-05-10 16:03:06 -07:00
Andrew Pareles 4068911d49 27 2025-05-10 16:02:56 -07:00
Andrew Pareles dcee2e2657 add prompt 2025-05-10 16:02:37 -07:00
Andrew Pareles 4c1c0b591a GoogleGenerativeAI (deprecated) -> genai, add gemini reasoning, improve reasoning logic 2025-05-10 15:59:26 -07:00
Andrew Pareles 61fc593903 shorten non URL links too 2025-05-10 15:53:05 -07:00
Andrew Pareles ecff59eed7 shorten long URI links 2025-05-10 15:29:55 -07:00
Ismail Pelaseyed e2a7b0e1a0 fix merge conflicts 2025-05-10 13:41:43 +02:00
Andrew Pareles 07c3f33f1e void increment 2025-05-10 03:36:54 -07:00
Andrew Pareles 0f93e1b055 Merge pull request #518 from voideditor/model-selection
Model selection
2025-05-10 03:33:45 -07:00
Andrew Pareles 2106573922 misc 2025-05-10 03:32:57 -07:00
Andrew Pareles 93a13a76af Merge remote-tracking branch 'origin/main' into model-selection 2025-05-10 03:31:45 -07:00
Andrew Pareles 7b18e25288 period 2025-05-10 03:29:33 -07:00
Andrew Pareles 57b0505a7e Merge pull request #517 from voideditor/revert-516-model-selection
apply UX message
2025-05-10 03:28:59 -07:00
Andrew Pareles e7436575a5 Revert "apply button failure better UX" 2025-05-10 03:28:39 -07:00
Andrew Pareles 58d95035ff update message 2025-05-10 03:28:29 -07:00
Andrew Pareles 96436ac461 Merge pull request #516 from voideditor/model-selection
apply button failure better UX
2025-05-10 03:26:48 -07:00
Andrew Pareles 85b1f0ec04 apply button failure better UX 2025-05-10 03:20:03 -07:00
Andrew Pareles 5232b4c30c update prompt to work better in agent 2025-05-10 02:28:40 -07:00
Andrew Pareles 81fd74d0b0 Merge pull request #515 from voideditor/model-selection
fix linux update message
2025-05-10 02:10:03 -07:00
Andrew Pareles 93b727198e fix linux update message 2025-05-10 02:09:49 -07:00
Andrew Pareles f33cf14b07 Update README.md 2025-05-09 17:13:35 -07:00
Andrew Pareles 15cf383524 Update README.md 2025-05-09 17:13:15 -07:00
Andrew Pareles f7489375b0 Update issue_template.md 2025-05-09 17:03:16 -07:00
Steven Wexler 08274637e1 Small touchups 2025-05-09 15:40:34 -06:00
Steven Wexler 168b92f6c4 Add Void: Copy Prompt as menu item to the ExplorerContext window 2025-05-09 15:38:32 -06:00
Ismail Pelaseyed a9aa9a26ea feat: add support for applying shellscripts and bash directly to terminal 2025-05-09 14:59:27 +02:00
Andrew Pareles eeb6167c12 Merge pull request #504 from voideditor/model-selection
do not show X / Check while streaming
2025-05-09 05:03:19 -07:00
Andrew Pareles c2092779e7 UI around tools 2025-05-09 05:02:08 -07:00
Andrew Pareles 19dc26d35b Merge pull request #502 from voideditor/model-selection
terminal 10s
2025-05-09 04:42:30 -07:00
Andrew Pareles b1c17256c2 terminal 10s 2025-05-09 04:41:52 -07:00
Andrew Pareles 1992a86211 Merge pull request #501 from voideditor/model-selection
Terminal improvements
2025-05-09 03:43:03 -07:00
Andrew Pareles 70fe79c4fe add terminal 2025-05-09 03:41:46 -07:00
Andrew Pareles a1bf83b5a6 faster 2025-05-09 03:35:18 -07:00
Andrew Pareles 215efed10c disposables 2025-05-09 02:03:23 -07:00
Andrew Pareles 6ada92e6f5 terminal tool improvements 2025-05-09 01:39:15 -07:00
Andrew Pareles ae55e97c18 add background terminal (not jittery) 2025-05-09 01:06:27 -07:00
Andrew Pareles 07c9a72c83 openrouter gemini fix 2025-05-08 23:53:19 -07:00
Andrew Pareles e577d4a01b Update README.md 2025-05-08 17:11:34 -07:00
Andrew Pareles 56c2f7a900 Merge pull request #491 from voideditor/model-selection
ui
2025-05-08 13:48:59 -07:00
Andrew Pareles 558bec1f61 not blue 2025-05-08 13:48:21 -07:00
Andrew Pareles 12e97f5689 ui 2025-05-08 13:34:23 -07:00
Andrew Pareles 7220323098 fix 2025-05-08 13:32:01 -07:00
Andrew Pareles 94229ff1d8 Merge pull request #490 from voideditor/model-selection
Onboarding UI
2025-05-08 13:22:21 -07:00
Andrew Pareles 3b778a3c36 spacing 2025-05-08 13:22:04 -07:00
Andrew Pareles f6529f2d76 better onboarding 2025-05-08 13:20:35 -07:00
Mathew Pareles 1b222e8335 Update README.md 2025-05-08 12:53:32 -07:00
Andrew Pareles 3b518e6f96 Merge pull request #486 from voideditor/model-selection
remove reapply
2025-05-08 12:12:49 -07:00
Andrew Pareles ac23cbb523 remove reapply 2025-05-08 12:11:54 -07:00
Andrew Pareles 301418bd12 Merge pull request #474 from voideditor/model-selection
Accept|Reject on Edit tool fix
2025-05-08 12:07:01 -07:00
Mathew Pareles d192bdcdc0 selecitons work with link 2025-05-08 04:22:50 -07:00
Mathew Pareles 42f2958959 desc1 link 2025-05-08 03:53:18 -07:00
Andrew Pareles 7a3cf4953d misc 2025-05-08 03:41:02 -07:00
Andrew Pareles 84ddf2b88c prompt 2025-05-08 02:40:13 -07:00
Andrew Pareles d00c478c5b add accept|reject on edit tool! 2025-05-08 02:21:57 -07:00
Mathew Pareles 42391a33c8 tooltip 2025-05-08 01:48:41 -07:00
Mathew Pareles 4618442c11 num messages better 2025-05-07 20:12:51 -07:00
Andrew Pareles 77d174edcf Merge pull request #471 from voideditor/model-selection
Misc fixes
2025-05-07 19:56:13 -07:00
Andrew Pareles 79d549b45b release fix 2025-05-07 19:52:54 -07:00
Mathew Pareles 8ba5ee5ea3 threads 2025-05-07 19:51:11 -07:00
Mathew Pareles 1d40c14d50 remove logging 2025-05-07 19:42:32 -07:00
Mathew Pareles cc51d2852e improve @ to mention 2025-05-07 19:41:41 -07:00
Andrew Pareles 8855cdeb04 Merge pull request #469 from GhostDog98/patch-3
Update HOW_TO_CONTRIBUTE.md with tip for nvm
2025-05-07 18:00:34 -07:00
ghostoverflow256 559994f1ee Update HOW_TO_CONTRIBUTE.md
Added a tip for those who may be unfamiliar with the node ecosystem, that you can use nvm to do that step (and probably should).
2025-05-08 10:46:19 +10:00
Andrew Pareles 05e3625014 Merge pull request #468 from voideditor/model-selection
Extensions
2025-05-07 14:37:15 -07:00
Andrew Pareles d9951e4108 dont delete, just transfer extensions 2025-05-07 14:35:26 -07:00
Andrew Pareles 620b5b7341 permalink 2025-05-06 14:13:33 -07:00
Andrew Pareles d33b5ff9a3 Merge pull request #465 from voideditor/model-selection
{} for openaicompat
2025-05-06 13:46:30 -07:00
Andrew Pareles 2208bce55f {} for openaicompat 2025-05-06 13:46:08 -07:00
Andrew Pareles bd62b68dfb Merge pull request #464 from voideditor/model-selection
Gemini Experimental
2025-05-06 13:35:04 -07:00
Andrew Pareles af3e0e863b exp free 2025-05-06 13:34:31 -07:00
Andrew Pareles 57f4d8cd38 Merge pull request #463 from voideditor/model-selection
Laggy Extensions Fix
2025-05-06 13:27:16 -07:00
Andrew Pareles 262f3aee12 update gemini models + fix del extension 2025-05-06 13:26:36 -07:00
Andrew Pareles b03cbadb9b auto delete laggy extensions 2025-05-06 13:16:57 -07:00
Andrew Pareles 390afaf227 ignore extensions actually works 2025-05-06 12:47:18 -07:00
Andrew Pareles 41f6ce8f9b Merge pull request #461 from voideditor/model-selection
update copy
2025-05-06 07:48:00 -07:00
Andrew Pareles cbc3d4b52f update copy 2025-05-06 07:41:58 -07:00
Andrew Pareles 38eeee8e30 Merge pull request #459 from voideditor/model-selection
Dump full files into context; better onboarding; better nav
2025-05-06 06:03:15 -07:00
Andrew Pareles 84f95170cb finish 2025-05-06 05:58:30 -07:00
Andrew Pareles 2de4cacf0f 1.3.2 2025-05-06 05:39:26 -07:00
Andrew Pareles 097d89366c file and folder dump! 2025-05-06 05:39:00 -07:00
Andrew Pareles fa83dfc529 onboarding improvements 2025-05-06 05:39:00 -07:00
Mathew Pareles c5862188cb goto next diff after accept/reject 2025-05-06 05:19:14 -07:00
Mathew Pareles 8816c7194d better tooltips 2025-05-06 05:09:44 -07:00
Mathew Pareles 4b40ac1b47 commandbar decent draft 2025-05-06 04:33:48 -07:00
Mathew Pareles 02115f5b6c bug in names 2025-05-06 03:52:28 -07:00
Mathew Pareles 786f5707e3 add keybinds 2025-05-06 03:47:21 -07:00
Andrew Pareles 71d703852b improve onboarding 2025-05-06 02:08:38 -07:00
Andrew Pareles ae179b8111 fix gemini OR 2025-05-05 22:05:01 -07:00
Andrew Pareles 170ccb9298 ui 2025-05-05 21:49:04 -07:00
Andrew Pareles a5b43ce146 Cmd+Shift+L transfers old text+selections, delete sidebarState 2025-05-05 21:44:42 -07:00
Mathew Pareles e0156803ff better commandbar ui 2025-05-05 21:37:41 -07:00
Andrew Pareles fcd445d875 Merge pull request #453 from voideditor/model-selection
Fix gemini OpenRouter
2025-05-05 17:17:57 -07:00
Andrew Pareles decd0c9bc4 openrouter gemini-style -> openai-style 2025-05-05 17:15:37 -07:00
Andrew Pareles e1da2a5231 update 2025-05-05 17:06:25 -07:00
Andrew Pareles 4570068fa9 update commnet 2025-05-05 17:06:06 -07:00
Andrew Pareles afacd32407 sourcecode overrides link 2025-05-05 17:02:35 -07:00
Andrew Pareles fa782243c9 comment 2025-05-05 17:01:06 -07:00
Andrew Pareles 19390808bc update 2025-05-05 17:00:29 -07:00
Andrew Pareles f77258799f update comment for VoidStaticModelInfo 2025-05-05 17:00:07 -07:00
Andrew Pareles f97cdd82d8 Merge pull request #451 from voideditor/model-selection
Azure, Reasoning, Model overrides, Fix Search/Replace without whitespace, File dump, New models
2025-05-05 16:42:05 -07:00
Andrew Pareles 9216a0e91f update voidVersion 2025-05-05 16:40:32 -07:00
Andrew Pareles aa03c34db6 update link 2025-05-05 16:39:52 -07:00
Andrew Pareles d125d8698b comment 2025-05-05 16:38:51 -07:00
Mathew Pareles f56ccad713 revert draft for today's push 2025-05-05 03:25:32 -07:00
Mathew Pareles 4c511f0c32 draft 2025-05-05 03:24:51 -07:00
Mathew Pareles ca3acb0d30 commandbar draft 2025-05-05 03:22:57 -07:00
Andrew Pareles 38e6ee94d8 phi 4 2025-05-05 03:09:38 -07:00
Andrew Pareles 1c9e684350 misc improvements 2025-05-05 02:52:21 -07:00
Mathew Pareles b6e3a57dc6 fix bug with widgetid 2025-05-05 02:30:07 -07:00
Mathew Pareles 7dc86502e3 remove text in @ to mention... 2025-05-05 02:30:07 -07:00
Andrew Pareles e70915afc3 fix model fallbacks - recognizedName 2025-05-05 02:23:27 -07:00
Andrew Pareles fab84d7371 style 2025-05-05 02:03:48 -07:00
Andrew Pareles d38b744dd2 small updates 2025-05-05 01:50:14 -07:00
Andrew Pareles b520523c4c current effort 2025-05-05 01:22:16 -07:00
Andrew Pareles b73c11429c finish adding effort budget 2025-05-05 01:15:53 -07:00
Andrew Pareles 7846b117bd model settings overrides part ii 2025-05-05 01:15:53 -07:00
Andrew Pareles 398a8a1934 misc + model overrides 2025-05-05 01:15:53 -07:00
Andrew Pareles 732269de87 misc 2025-05-05 01:15:52 -07:00
Andrew Pareles 4dae90e047 maxOutputTokens -> reservedOutputTokenSpace 2025-05-05 01:15:52 -07:00
Mathew Pareles 25c12b101f small 2025-05-05 01:10:10 -07:00
Mathew Pareles 09d7e7f3e6 accept/reject better keybind 2025-05-05 00:57:00 -07:00
Mathew Pareles c088ad3c07 keyboard shortcuts for accept/reject 2025-05-05 00:57:00 -07:00
Mathew Pareles 9bebbb5615 draft for accept/reject interactive 2025-05-05 00:57:00 -07:00
Andrew Pareles e726c3c5de add system message trimming because of very small models with 4096 window len 2025-05-04 19:33:29 -07:00
Andrew Pareles d826052444 add model overrides 2025-05-04 19:33:29 -07:00
Mathew Pareles d841468f3b diffidx updates properly 2025-05-03 18:47:01 -07:00
Mathew Pareles 4d04141139 fix backspace 2025-05-03 17:09:43 -07:00
Andrew Pareles 5090e31a89 add model overrides and headers JSON! 2025-05-03 16:41:41 -07:00
Andrew Pareles 4fb0d7d727 providerName 2025-05-02 15:56:36 -07:00
Andrew Pareles 5f12c7a1cf fix submit button click 2025-05-01 22:04:33 -07:00
Andrew Pareles 1af393983b rewrite file tool streaming fix 2025-05-01 21:52:38 -07:00
Andrew Pareles d3d62652db copy 2025-05-01 21:47:54 -07:00
Andrew Pareles 5cd1fa15cf copy 2025-05-01 18:04:35 -07:00
Andrew Pareles 14a276e4a0 flatten settings 2025-05-01 04:04:10 -07:00
Andrew Pareles cf0728f4c6 Merge pull request #443 from voideditor/model-selection
Chat interruption state
2025-05-01 00:45:57 -07:00
Andrew Pareles 6a119e1246 add logs for transfer 2025-05-01 00:45:22 -07:00
Andrew Pareles 8723107f1d voidrules 2025-05-01 00:36:14 -07:00
Andrew Pareles b1ca03611e interruptor and idle chat state fixes 2025-05-01 00:33:14 -07:00
Mathew Pareles 32f41f97d9 bg decent 2025-05-01 00:23:00 -07:00
Mathew Pareles 3580eb01d0 stagingselection bg 2025-04-30 23:05:39 -07:00
Andrew Pareles cb549ddd9a OpenAI-Compatible -> Custom 2025-04-30 22:43:57 -07:00
Andrew Pareles f0f7c57485 voidrules 2025-04-30 22:31:06 -07:00
Andrew Pareles ace3bfdfb6 Merge pull request #441 from voideditor/model-selection
Issue template
2025-04-30 22:28:53 -07:00
Andrew Pareles 90c1beecca do not enable blank issues 2025-04-30 22:28:11 -07:00
Mathew Pareles 4ad286a030 allow user to search from the start of @ file 2025-04-30 22:23:54 -07:00
Andrew Pareles c5bf5e453e Merge pull request #439 from voideditor/model-selection
Edit fixes
2025-04-30 22:14:17 -07:00
Andrew Pareles a89934d7ad gemini 2025-04-30 22:11:58 -07:00
Andrew Pareles e424a2cdbd fix double cancel 2025-04-30 22:00:52 -07:00
Andrew Pareles d9f2fcd700 error UI less overwhelming, gemini separated system message, improve Apply prompt 2025-04-30 21:42:29 -07:00
Mathew Pareles 1ed82ba174 dont show propective selections since we have @ to mention 2025-04-30 21:31:30 -07:00
Mathew Pareles 80a5dc0362 allow backspace stagingselections 2025-04-30 21:28:22 -07:00
Andrew Pareles f166fa3581 native gemini tools, editing UI, tool prompts 2025-04-30 20:27:08 -07:00
Andrew Pareles cec0df989c onerror for edits 2025-04-30 20:27:08 -07:00
Mathew Pareles ceba02ed47 fix jittery @ to mention 2025-04-30 19:26:54 -07:00
Mathew Pareles 1b9fe5b16d bad spacing 2025-04-30 16:47:34 -07:00
Andrew Pareles 8d8fcba833 generating... 2025-04-30 15:14:14 -07:00
Andrew Pareles e02d1e6847 Merge pull request #300 from Brandon-Bai/feature/add-chat-history-removal-button
Add history chat thread remove button in the chat side nav
2025-04-30 14:28:31 -07:00
Andrew Pareles b68f34e644 Merge remote-tracking branch 'origin/main' into pr/Brandon-Bai/300 2025-04-30 13:59:11 -07:00
Andrew Pareles f13279e84a temp 2025-04-30 13:58:51 -07:00
Andrew Pareles 9b861db77f rm debug 2025-04-30 13:49:09 -07:00
Andrew Pareles a8ba44e49e QwQ 2025-04-30 13:48:16 -07:00
Andrew Pareles 1632bb5169 qwen 3 2025-04-30 13:48:07 -07:00
Andrew Pareles 125968feb1 Merge remote-tracking branch 'origin/main' into model-selection 2025-04-30 13:41:59 -07:00
Andrew Pareles d2310973ec qwen 3 2025-04-30 13:39:43 -07:00
Andrew Pareles 722da01c7c Update README.md 2025-04-29 03:55:35 -07:00
Andrew Pareles 244cffe873 Update VOID_CODEBASE_GUIDE.md 2025-04-29 03:53:40 -07:00
Andrew Pareles d5e76813ce Update VOID_CODEBASE_GUIDE.md 2025-04-29 03:52:48 -07:00
Andrew Pareles a663a258f7 Update VOID_CODEBASE_GUIDE.md 2025-04-29 03:52:02 -07:00
Andrew Pareles 51b66338b5 Update README.md 2025-04-29 03:51:19 -07:00
Andrew Pareles b91b2b9276 Update README.md 2025-04-29 03:51:10 -07:00
Andrew Pareles 1081878c31 Update README.md 2025-04-29 03:50:27 -07:00
Andrew Pareles 477dedeb38 Update HOW_TO_CONTRIBUTE.md 2025-04-29 03:49:36 -07:00
Andrew Pareles df721c5370 Merge pull request #414 from Dylan-86/patch-3
Update HOW_TO_CONTRIBUTE.md
2025-04-29 00:59:00 -07:00
Andrew Pareles e493e65cff update 2025-04-29 00:58:22 -07:00
Andrew Pareles c16a45c647 Merge remote-tracking branch 'origin/main' into pr/Dylan-86/414 2025-04-29 00:54:55 -07:00
Andrew Pareles 9871d4f413 update 2025-04-29 00:50:04 -07:00
Andrew Pareles e94c0fb1c0 Merge pull request #436 from voideditor/model-selection
Updates + Rebase
2025-04-29 00:36:35 -07:00
Andrew Pareles 44398eb96b Merge pull request #436 from voideditor/model-selection
Updates + Rebase
2025-04-29 00:36:35 -07:00
Andrew Pareles 413be993d8 interruptions work 2025-04-29 00:25:16 -07:00
Andrew Pareles 106b30dfbd interruptions work 2025-04-29 00:25:16 -07:00
Andrew Pareles 58804620f7 capture toolName 2025-04-29 00:20:13 -07:00
Andrew Pareles 02b03003c0 capture toolName 2025-04-29 00:20:13 -07:00
Andrew Pareles 46d318b0ce voideditor-test -> voideditor 2025-04-29 00:12:46 -07:00
Andrew Pareles f9d8e92621 voideditor-test -> voideditor 2025-04-29 00:12:46 -07:00
Andrew Pareles 806b3b90b5 gitignore 2025-04-29 00:11:57 -07:00
Andrew Pareles 25f1fb4d19 gitignore 2025-04-29 00:11:57 -07:00
Andrew Pareles dfe00e7a18 HOW_TO_CONTRIBUTE 2025-04-29 00:09:40 -07:00
Andrew Pareles 2bd5708915 HOW_TO_CONTRIBUTE 2025-04-29 00:09:40 -07:00
Andrew Pareles c666a5f721 COPY FROM voideditor-test/void (void-test)!!!! 2025-04-29 00:07:20 -07:00
Andrew Pareles d31c0730ed rebase from 17baf84 2025-04-29 00:07:20 -07:00
Dylan-86 a146b8ba0d Update HOW_TO_CONTRIBUTE.md
I added some notes about Ubuntu installing as I ran through a simple npm blocking error when a folder in the Void path contains a space " " like "New Folder". I simply moved to "NewFolder" and the error disappeared.
2025-04-17 12:58:07 +02:00
Shi Bai cbae8d5ec2 Remove one empty line between methods 2025-03-04 21:07:54 -08:00
Andrew Pareles 9f25d652d5 merge 2025-03-01 18:46:10 -08:00
Andrew Pareles be291fd0f9 Merge remote-tracking branch 'origin/main' into pr/Brandon-Bai/300 2025-03-01 18:46:07 -08:00
Andrew Pareles 1ef22641b7 minor changes 2025-03-01 18:19:33 -08:00
Andrew Pareles 334401eb50 Merge remote-tracking branch 'origin/main' into pr/Brandon-Bai/300 2025-03-01 17:52:28 -08:00
Shi Bai f287a5388f Add history chat thread remove button in the chat side nav
- Added a trash icon to the right of the chat thread name to remove a thread history.
- Added 2 icon size constants in SidebarThreadSelector.tsx to avoid the usage of magic numbers.
2025-02-28 16:08:43 -08:00
59 changed files with 12757 additions and 6910 deletions
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+1 -1
View File
@@ -6,4 +6,4 @@ title: For VSCode-related issues (eg builds), please start the title with `[App]
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
2. Describe the issue/feature here.
2. Describe the issue/feature here!
+7
View File
@@ -21,3 +21,10 @@ vscode.db
product.overrides.json
*.snap.actual
.vscode-test
# Void added these:
.tmp/
.tmp2/
.tool-versions
src/vs/workbench/contrib/void/browser/react/out/**
src/vs/workbench/contrib/void/browser/react/src2/**
+10
View File
@@ -0,0 +1,10 @@
This is a fork of the VSCode repo called Void.
Most code we care about lives in src/vs/workbench/contrib/void.
You may often need to explore the full repo to find relevant parts of code.
Look for services and built-in functions that you might need to use to solve the problem.
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
+14 -11
View File
@@ -12,7 +12,7 @@ There are a few ways to contribute:
### Codebase Guide
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to add new features.
The repo is not as intimidating as it first seems if you read the guide!
@@ -52,17 +52,18 @@ First, run `npm install -g node-gyp`. Then:
### d. Building Void from inside VSCode
To build Void, open `void/` inside VSCode. Then open your terminal and run:
1. `npm install` to install all dependencies.
2. Build Void.
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
1. `git clone https://github.com/voideditor/void` to clone the repo.
2. `npm install` to install all dependencies.
3. To build Void, open VSCode. Then:
- Windows: Press <kbd>Ctrl+Shift+B</kbd>.
- Mac: Press <kbd>Cmd+Shift+B</kbd>.
- Linux: Press <kbd>Ctrl+Shift+B</kbd>.
- This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code).
3. Run Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
4. Nice-to-knows.
4. To run Void:
- Windows: `./scripts/code.bat`.
- Mac: `./scripts/code.sh`.
- Linux: `./scripts/code.sh`.
5. 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.
- You can kill any of the build scripts by pressing `Ctrl+D` in VSCode terminal. If you press `Ctrl+C` the script will close but will keep running in the background (to open all background scripts, just re-build).
@@ -85,6 +86,8 @@ To build Void from the terminal instead of from inside VSCode, follow the steps
- Make sure you followed the prerequisite steps above.
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`)!
- You can do this easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc`.
- Make sure that the path to your Void folder does not have any spaces in it.
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`.
- If you see missing styles, wait a few seconds and then reload.
+10 -13
View File
@@ -11,30 +11,27 @@
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!
Use AI agents on your codebase, checkpoint and visualize changes, and bring any model or host locally. Void sends messages directly to providers without retaining your data.
This repo contains the full sourcecode for Void. If you're new, welcome!
- 🧭 [Website](https://voideditor.com)
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
- 🚙 [Project Board](https://github.com/orgs/voideditor/projects/2)
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
- 📝 [Changelog](https://voideditor.com/changelog)
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
## Contributing
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
1. To get started working on Void, check out our Project Board! You can also see [HOW_TO_CONTRIBUTE](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
3. We're open to collaborations and suggestions of all types - just reach out.
2. Feel free to attend a casual weekly meeting in our Discord channel!
## Reference
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the codebase, see [VOID_CODEBASE_GUIDE](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
## Support
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
You can always reach us in our Discord server or contact us via email: hello@voideditor.com.
+4 -8
View File
@@ -132,25 +132,24 @@ If you want to know how our build pipeline works, see our build repo [here](http
## VSCode Codebase Guide
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
For additional references, the Void team put together this list of links to get up and running with VSCode.
<details>
#### Links for Beginners
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
#### Links for Contributors
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
#### Misc
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
@@ -159,13 +158,10 @@ The Void team put together this list of links to get up and running with VSCode'
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
</details>
-1
View File
@@ -82,7 +82,6 @@ function buildWin32Setup(arch, target) {
productJson['target'] = target;
fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t'));
console.log('RawVersion!!!!!!!!!!!!!!', pkg.version.replace(/-\w+$/, '')) // Void
const quality = product.quality || 'dev';
const definitions = {
NameLong: product.nameLong,
+1 -1
View File
@@ -71,7 +71,7 @@
"type": "string",
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
"scope": "application",
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
},
"remote.SSH.remotePlatform": {
"type": "object",
+165 -165
View File
@@ -8,203 +8,203 @@ import { getVSCodeServerConfig } from './serverConfig';
import SSHConnection from './ssh/sshConnection';
export interface ServerInstallOptions {
id: string;
quality: string;
commit: string;
version: string;
release?: string; // void specific
extensionIds: string[];
envVariables: string[];
useSocketPath: boolean;
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate: string;
id: string;
quality: string;
commit: string;
version: string;
release?: string; // void specific
extensionIds: string[];
envVariables: string[];
useSocketPath: boolean;
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate: string;
}
export interface ServerInstallResult {
exitCode: number;
listeningOn: number | string;
connectionToken: string;
logFile: string;
osReleaseId: string;
arch: string;
platform: string;
tmpDir: string;
[key: string]: any;
exitCode: number;
listeningOn: number | string;
connectionToken: string;
logFile: string;
osReleaseId: string;
arch: string;
platform: string;
tmpDir: string;
[key: string]: any;
}
export class ServerInstallError extends Error {
constructor(message: string) {
super(message);
}
constructor(message: string) {
super(message);
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
let shell = 'powershell';
let shell = 'powershell';
// detect platform and shell for windows
if (!platform || platform === 'windows') {
const result = await conn.exec('uname -s');
// detect platform and shell for windows
if (!platform || platform === 'windows') {
const result = await conn.exec('uname -s');
if (result.stdout) {
if (result.stdout.includes('windows32')) {
platform = 'windows';
} else if (result.stdout.includes('MINGW64')) {
platform = 'windows';
shell = 'bash';
}
} else if (result.stderr) {
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
platform = 'windows';
}
if (result.stdout) {
if (result.stdout.includes('windows32')) {
platform = 'windows';
} else if (result.stdout.includes('MINGW64')) {
platform = 'windows';
shell = 'bash';
}
} else if (result.stderr) {
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
platform = 'windows';
}
if (result.stderr.includes('is not recognized as an internal or external command')) {
platform = 'windows';
shell = 'cmd';
}
}
if (result.stderr.includes('is not recognized as an internal or external command')) {
platform = 'windows';
shell = 'cmd';
}
}
if (platform) {
logger.trace(`Detected platform: ${platform}, ${shell}`);
}
}
if (platform) {
logger.trace(`Detected platform: ${platform}, ${shell}`);
}
}
const scriptId = crypto.randomBytes(12).toString('hex');
const scriptId = crypto.randomBytes(12).toString('hex');
const vscodeServerConfig = await getVSCodeServerConfig();
const installOptions: ServerInstallOptions = {
id: scriptId,
version: vscodeServerConfig.version,
commit: vscodeServerConfig.commit,
quality: vscodeServerConfig.quality,
release: vscodeServerConfig.release,
extensionIds,
envVariables,
useSocketPath,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
};
const vscodeServerConfig = await getVSCodeServerConfig();
const installOptions: ServerInstallOptions = {
id: scriptId,
version: vscodeServerConfig.version,
commit: vscodeServerConfig.commit,
quality: vscodeServerConfig.quality,
release: vscodeServerConfig.release,
extensionIds,
envVariables,
useSocketPath,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
};
let commandOutput: { stdout: string; stderr: string };
if (platform === 'windows') {
const installServerScript = generatePowerShellInstallScript(installOptions);
let commandOutput: { stdout: string; stderr: string };
if (platform === 'windows') {
const installServerScript = generatePowerShellInstallScript(installOptions);
logger.trace('Server install command:', installServerScript);
logger.trace('Server install command:', installServerScript);
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
const endRegex = new RegExp(`${scriptId}: end`);
// investigate if it's possible to use `-EncodedCommand` flag
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
let command = '';
if (shell === 'powershell') {
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'bash') {
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'cmd') {
const script = installServerScript.trim()
// remove comments
.replace(/^#.*$/gm, '')
// remove empty lines
.replace(/\n{2,}/gm, '\n')
// remove leading spaces
.replace(/^\s*/gm, '')
// escape double quotes (from powershell/cmd)
.replace(/"/g, '"""')
// escape single quotes (from cmd)
.replace(/'/g, `''`)
// escape redirect (from cmd)
.replace(/>/g, `^>`)
// escape new lines (from powershell/cmd)
.replace(/\n/g, '\'`n\'');
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
const endRegex = new RegExp(`${scriptId}: end`);
// investigate if it's possible to use `-EncodedCommand` flag
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
let command = '';
if (shell === 'powershell') {
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'bash') {
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'cmd') {
const script = installServerScript.trim()
// remove comments
.replace(/^#.*$/gm, '')
// remove empty lines
.replace(/\n{2,}/gm, '\n')
// remove leading spaces
.replace(/^\s*/gm, '')
// escape double quotes (from powershell/cmd)
.replace(/"/g, '"""')
// escape single quotes (from cmd)
.replace(/'/g, `''`)
// escape redirect (from cmd)
.replace(/>/g, `^>`)
// escape new lines (from powershell/cmd)
.replace(/\n/g, '\'`n\'');
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
logger.trace('Command length (8191 max):', command.length);
logger.trace('Command length (8191 max):', command.length);
if (command.length > 8191) {
throw new ServerInstallError(`Command line too long`);
}
} else {
throw new ServerInstallError(`Not supported shell: ${shell}`);
}
if (command.length > 8191) {
throw new ServerInstallError(`Command line too long`);
}
} else {
throw new ServerInstallError(`Not supported shell: ${shell}`);
}
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
} else {
const installServerScript = generateBashInstallScript(installOptions);
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
} else {
const installServerScript = generateBashInstallScript(installOptions);
logger.trace('Server install command:', installServerScript);
// Fish shell does not support heredoc so let's workaround it using -c option,
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
}
logger.trace('Server install command:', installServerScript);
// Fish shell does not support heredoc so let's workaround it using -c option,
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
}
if (commandOutput.stderr) {
logger.trace('Server install command stderr:', commandOutput.stderr);
}
logger.trace('Server install command stdout:', commandOutput.stdout);
if (commandOutput.stderr) {
logger.trace('Server install command stderr:', commandOutput.stderr);
}
logger.trace('Server install command stdout:', commandOutput.stdout);
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
if (!resultMap) {
throw new ServerInstallError(`Failed parsing install script output`);
}
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
if (!resultMap) {
throw new ServerInstallError(`Failed parsing install script output`);
}
const exitCode = parseInt(resultMap.exitCode, 10);
if (exitCode !== 0) {
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
}
const exitCode = parseInt(resultMap.exitCode, 10);
if (exitCode !== 0) {
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
}
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
? parseInt(resultMap.listeningOn, 10)
: resultMap.listeningOn;
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
? parseInt(resultMap.listeningOn, 10)
: resultMap.listeningOn;
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
return {
exitCode,
listeningOn,
connectionToken: resultMap.connectionToken,
logFile: resultMap.logFile,
osReleaseId: resultMap.osReleaseId,
arch: resultMap.arch,
platform: resultMap.platform,
tmpDir: resultMap.tmpDir,
...remoteEnvVars
};
return {
exitCode,
listeningOn,
connectionToken: resultMap.connectionToken,
logFile: resultMap.logFile,
osReleaseId: resultMap.osReleaseId,
arch: resultMap.arch,
platform: resultMap.platform,
tmpDir: resultMap.tmpDir,
...remoteEnvVars
};
}
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
const startResultStr = `${scriptId}: start`;
const endResultStr = `${scriptId}: end`;
const startResultStr = `${scriptId}: start`;
const endResultStr = `${scriptId}: end`;
const startResultIdx = str.indexOf(startResultStr);
if (startResultIdx < 0) {
return undefined;
}
const startResultIdx = str.indexOf(startResultStr);
if (startResultIdx < 0) {
return undefined;
}
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
if (endResultIdx < 0) {
return undefined;
}
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
if (endResultIdx < 0) {
return undefined;
}
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
const resultMap: { [k: string]: string } = {};
const resultArr = installResult.split(/\r?\n/);
for (const line of resultArr) {
const [key, value] = line.split('==');
resultMap[key] = value;
}
const resultMap: { [k: string]: string } = {};
const resultArr = installResult.split(/\r?\n/);
for (const line of resultArr) {
const [key, value] = line.split('==');
resultMap[key] = value;
}
return resultMap;
return resultMap;
}
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
return `
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
return `
# Server installation script
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
@@ -427,16 +427,16 @@ print_install_results_and_exit 0
}
function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const downloadUrl = serverDownloadUrlTemplate
.replace(/\$\{quality\}/g, quality)
.replace(/\$\{version\}/g, version)
.replace(/\$\{commit\}/g, commit)
.replace(/\$\{os\}/g, 'win32')
.replace(/\$\{arch\}/g, 'x64')
.replace(/\$\{release\}/g, release ?? '');
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const downloadUrl = serverDownloadUrlTemplate
.replace(/\$\{quality\}/g, quality)
.replace(/\$\{version\}/g, version)
.replace(/\$\{commit\}/g, commit)
.replace(/\$\{os\}/g, 'win32')
.replace(/\$\{arch\}/g, 'x64')
.replace(/\$\{release\}/g, release ?? '');
return `
return `
# Server installation script
$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())"
+1 -1
View File
@@ -38,7 +38,7 @@
"type": "string",
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
"scope": "application",
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
}
}
},
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
const scriptId = crypto.randomBytes(12).toString('hex');
+7538 -3910
View File
File diff suppressed because it is too large Load Diff
+2 -1
View File
@@ -75,7 +75,7 @@
"@anthropic-ai/sdk": "^0.40.0",
"@c4312/eventsource-umd": "^3.0.5",
"@floating-ui/react": "^0.27.8",
"@google/generative-ai": "^0.24.0",
"@google/genai": "^0.13.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.6.0",
@@ -109,6 +109,7 @@
"cross-spawn": "^7.0.6",
"diff": "^7.0.0",
"eslint-plugin-react": "^7.37.5",
"google-auth-library": "^9.15.1",
"groq-sdk": "^0.20.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
+5 -2
View File
@@ -1,7 +1,8 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.2.8",
"voidVersion": "1.3.7",
"voidRelease": "0031",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
@@ -38,6 +39,8 @@
"builtInExtensions": [],
"linkProtectionTrustedDomains": [
"https://voideditor.com",
"https://voideditor.dev"
"https://voideditor.dev",
"https://github.com/voideditor/void",
"https://ollama.com"
]
}
@@ -4,3 +4,23 @@
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
export const VOID_ACCEPT_DIFF_ACTION_ID = 'void.acceptDiff'
export const VOID_REJECT_DIFF_ACTION_ID = 'void.rejectDiff'
export const VOID_GOTO_NEXT_DIFF_ACTION_ID = 'void.goToNextDiff'
export const VOID_GOTO_PREV_DIFF_ACTION_ID = 'void.goToPrevDiff'
export const VOID_GOTO_NEXT_URI_ACTION_ID = 'void.goToNextUri'
export const VOID_GOTO_PREV_URI_ACTION_ID = 'void.goToPrevUri'
export const VOID_ACCEPT_FILE_ACTION_ID = 'void.acceptFile'
export const VOID_REJECT_FILE_ACTION_ID = 'void.rejectFile'
export const VOID_ACCEPT_ALL_DIFFS_ACTION_ID = 'void.acceptAllDiffs'
export const VOID_REJECT_ALL_DIFFS_ACTION_ID = 'void.rejectAllDiffs'
@@ -790,6 +790,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
console.log('starting autocomplete...', predictionType)
const featureName: FeatureName = 'Autocomplete'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -807,6 +808,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}),
modelSelection,
modelSelectionOptions,
overridesOfModel,
logging: { loggingName: 'Autocomplete' },
onText: () => { }, // unused in FIMMessage
// onText: async ({ fullText, newText }) => {
@@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
@@ -35,6 +35,8 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
import { timeout } from '../../../../base/common/async.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { IFileService } from '../../../../platform/files/common/files.js';
// related to retrying when LLM message has error
@@ -100,6 +102,13 @@ const defaultMessageState: UserMessageState = {
// a 'thread' means a chat message history
type WhenMounted = {
textAreaRef: { current: HTMLTextAreaElement | null }; // the textarea that this thread has, gets set in SidebarChat
scrollToBottom: () => void;
}
export type ThreadType = {
id: string; // store the id here too
createdAt: string; // ISO string
@@ -120,6 +129,15 @@ export type ThreadType = {
[codespanName: string]: CodespanLocationLink
}
}
mountedInfo?: {
whenMounted: Promise<WhenMounted>
_whenMountedResolver: (res: WhenMounted) => void
mountedIsResolvedRef: { current: boolean };
}
};
}
@@ -180,7 +198,7 @@ export type ThreadStreamState = {
error?: undefined;
llmInfo?: undefined;
toolInfo?: undefined;
interrupt?: undefined;
interrupt: 'not_needed' | Promise<() => void>; // calling this should have no effect on state - would be too confusing. it just cancels the tool
}
}
@@ -235,6 +253,7 @@ export interface IChatThreadService {
isCurrentlyFocusingMessage(): boolean;
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
popStagingSelections(numPops?: number): void;
addNewStagingSelection(newSelection: StagingSelectionItem): void;
dangerousSetState: (newState: ThreadsState) => void;
@@ -266,6 +285,9 @@ export interface IChatThreadService {
// jump to history
jumpToCheckpointBeforeMessageIdx(opts: { threadId: string, messageIdx: number, jumpToUserModified: boolean }): void;
focusCurrentChat: () => Promise<void>
blurCurrentChat: () => Promise<void>
}
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
@@ -299,6 +321,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@INotificationService private readonly _notificationService: INotificationService,
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
@IFileService private readonly _fileService: IFileService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@@ -332,6 +356,26 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
async focusCurrentChat() {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
const s = await thread.state.mountedInfo?.whenMounted
if (!this.isCurrentlyFocusingMessage()) {
s?.textAreaRef.current?.focus()
}
}
async blurCurrentChat() {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
const s = await thread.state.mountedInfo?.whenMounted
if (!this.isCurrentlyFocusingMessage()) {
s?.textAreaRef.current?.blur()
}
}
dangerousSetState = (newState: ThreadsState) => {
this.state = newState
@@ -376,7 +420,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
private _setState(state: Partial<ThreadsState>, doNotRefreshMountInfo?: boolean) {
const newState = {
...this.state,
...state
@@ -384,8 +428,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this.state = newState
if (affectsCurrent)
this._onDidChangeCurrentThread.fire()
this._onDidChangeCurrentThread.fire()
// if we just switched to a thread, update its current stream state if it's not streaming to possibly streaming
@@ -407,6 +450,27 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
// if we did not just set the state to true, set mount info
if (doNotRefreshMountInfo) return
let whenMountedResolver: (w: WhenMounted) => void
const whenMountedPromise = new Promise<WhenMounted>((res) => whenMountedResolver = res)
this._setThreadState(threadId, {
mountedInfo: {
whenMounted: whenMountedPromise,
mountedIsResolvedRef: { current: false },
_whenMountedResolver: (w: WhenMounted) => {
whenMountedResolver(w)
const mountInfo = this.state.allThreads[threadId]?.state.mountedInfo
if (mountInfo) mountInfo.mountedIsResolvedRef.current = true
},
}
}, true) // do not trigger an update
}
@@ -476,7 +540,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const { name, id, rawParams } = lastMsg
const errorMessage = this.errMsgs.rejected
const errorMessage = this.toolErrMsgs.rejected
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
this._setStreamState(threadId, undefined)
}
@@ -485,34 +549,42 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
// interrupt any effects
const interrupt = await this.streamState[threadId]?.interrupt
interrupt?.()
// add assistant message
if (this.streamState[threadId]?.isRunning === 'LLM') {
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._addUserCheckpoint({ threadId })
}
// add tool that's running
else if (this.streamState[threadId]?.isRunning === 'tool') {
const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo
const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo
const content = content_ || this.toolErrMsgs.interrupted
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null })
}
// reject the tool for the user if relevant
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
this.rejectLatestToolRequest(threadId)
}
else if (this.streamState[threadId]?.isRunning === 'idle') {
// do nothing
}
this._addUserCheckpoint({ threadId })
// interrupt any effects
const interrupt = await this.streamState[threadId]?.interrupt
if (typeof interrupt === 'function')
interrupt()
this._setStreamState(threadId, undefined)
}
private readonly errMsgs = {
private readonly toolErrMsgs = {
rejected: 'Tool call was rejected by the user.',
interrupted: 'Tool call was interrupted by the user.',
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
}
@@ -596,15 +668,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
finally {
this._setStreamState(threadId, undefined)
}
// 4. stringify the result to give to the LLM
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
const errorMessage = this.toolErrMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
return {}
}
@@ -631,11 +700,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}) {
let interruptedWhenIdle = false
const idleInterruptor = Promise.resolve(() => { interruptedWhenIdle = true })
// _runToolCall does not need setStreamState({idle}) before it, but it needs it after it. (handles its own setStreamState)
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
// not running at start, clear state
this._setStreamState(threadId, { isRunning: 'idle' })
const { overridesOfModel } = this._settingsService.state
let nMessagesSent = 0
let shouldSendAnotherMessage = true
@@ -644,9 +715,14 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
this._setStreamState(threadId, undefined)
if (interrupted) { return }
if (interrupted) {
this._setStreamState(threadId, undefined)
this._addUserCheckpoint({ threadId })
}
}
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
// tool use loop
while (shouldSendAnotherMessage) {
@@ -655,6 +731,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
isRunningWhenEnd = undefined
nMessagesSent += 1
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
chatMessages,
@@ -662,63 +740,46 @@ class ChatThreadService extends Disposable implements IChatThreadService {
chatMode
})
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
let shouldRetryLLM = true
let nAttempts = 0
while (shouldRetryLLM) {
if (this.streamState[threadId]?.isRunning) {
// if already streaming, stop
console.log('returning...', this.streamState[threadId])
return
}
shouldRetryLLM = false
nAttempts += 1
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
type ResTypes =
| { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } }
| { type: 'llmError', error?: { message: string; fullError: Error | null; } }
| { type: 'llmAborted' }
let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<ResTypes>((res, rej) => { resMessageIsDonePromise = res })
let aborted = false
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages: messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) })
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, undefined)
resMessageIsDonePromise(toolCall) // resolve with tool calls
resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls
},
onError: async (error) => {
if (this.streamState[threadId]?.isRunning !== 'LLM') {
console.log('Unexpected onError when', this.streamState[threadId]?.isRunning)
return
}
if (nAttempts < CHAT_RETRIES) {
nAttempts += 1
shouldRetryLLM = true
this._setStreamState(threadId, undefined) // clear later so can be interrupted
resMessageIsDonePromise()
}
else {
// add assistant's message to chat history, and clear selection
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._setStreamState(threadId, { isRunning: undefined, error })
resMessageIsDonePromise()
}
resMessageIsDonePromise({ type: 'llmError', error: error })
},
onAbort: () => {
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
aborted = true
this._setStreamState(threadId, { isRunning: 'idle' })
resMessageIsDonePromise()
resMessageIsDonePromise({ type: 'llmAborted' })
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
},
})
@@ -730,31 +791,64 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) })
const toolCall = await messageIsDonePromise // wait for message to complete
const llmRes = await messageIsDonePromise // wait for message to complete
if (aborted) {
// if something else started running in the meantime
if (this.streamState[threadId]?.isRunning !== 'LLM') {
// console.log('Chat thread interrupted by a newer chat thread', this.streamState[threadId]?.isRunning)
return
}
// llm res aborted
if (llmRes.type === 'llmAborted') {
this._setStreamState(threadId, undefined)
return
}
if (shouldRetryLLM) {
this._setStreamState(threadId, { isRunning: 'idle' })
await timeout(RETRY_DELAY)
continue
// llm res error
else if (llmRes.type === 'llmError') {
// error, should retry
if (nAttempts < CHAT_RETRIES) {
shouldRetryLLM = true
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
await timeout(RETRY_DELAY)
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
else
continue // retry
}
// error, but too many attempts
else {
const { error } = llmRes
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._setStreamState(threadId, { isRunning: undefined, error })
this._addUserCheckpoint({ threadId })
return
}
}
this._setStreamState(threadId, { isRunning: 'idle' })
// llm res success
const { toolCall, info } = llmRes
this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning })
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity
// call tool if there is one
if (toolCall) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
if (interrupted) {
this._setStreamState(threadId, undefined)
return
}
this._setStreamState(threadId, { isRunning: 'idle' })
if (awaitingUserApproval) { isRunningWhenEnd = 'awaiting_user' }
else { shouldSendAnotherMessage = true }
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
}
} // end while (attempts)
@@ -764,8 +858,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { isRunning: isRunningWhenEnd })
// add checkpoint before the next user message
if (!isRunningWhenEnd)
this._addUserCheckpoint({ threadId })
if (!isRunningWhenEnd) this._addUserCheckpoint({ threadId })
// capture number of messages sent
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
@@ -801,7 +894,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
}
@@ -1069,7 +1162,10 @@ We only need to do it for files that were edited since `from`, ie files between
class: undefined,
run: () => {
this.switchToThread(threadId)
// TODO!!! scroll to bottom
// scroll to bottom
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
m.scrollToBottom()
})
}
}]
},
@@ -1095,7 +1191,6 @@ We only need to do it for files that were edited since `from`, ie files between
// interrupt existing stream
if (this.streamState[threadId]?.isRunning) {
console.log('stopping....')
await this.abortRunning(threadId)
}
@@ -1104,14 +1199,12 @@ We only need to do it for files that were edited since `from`, ie files between
this._addUserCheckpoint({ threadId })
}
const { chatMode } = this._settingsService.state.globalSettings
// add user's message to chat history
const instructions = userMessage
const currSelns: StagingSelectionItem[] = _chatSelections ?? thread.state.stagingSelections
const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const
const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content)
const userMessageContent = await chat_userMessageContent(instructions, currSelns, { directoryStrService: this._directoryStringService, fileService: this._fileService }) // user message + names of files (NOT content)
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
@@ -1121,6 +1214,11 @@ We only need to do it for files that were edited since `from`, ie files between
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
threadId,
)
// scroll to bottom
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
m.scrollToBottom()
})
}
@@ -1143,7 +1241,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
};
this._storeAllThreads(newThreads);
this._setState({ allThreads: newThreads }, true);
this._setState({ allThreads: newThreads });
}
// Now call the original method to add the user message and stream the response
@@ -1173,7 +1271,7 @@ We only need to do it for files that were edited since `from`, ie files between
messages: slicedMessages
}
}
}, true)
})
// re-add the message and stream it
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
@@ -1446,7 +1544,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}, true)
})
}
@@ -1477,7 +1575,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
switchToThread(threadId: string) {
this._setState({ currentThreadId: threadId }, true)
this._setState({ currentThreadId: threadId })
}
@@ -1500,7 +1598,7 @@ We only need to do it for files that were edited since `from`, ie files between
[newThread.id]: newThread
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id })
}
@@ -1513,7 +1611,7 @@ We only need to do it for files that were edited since `from`, ie files between
// store the updated threads
this._storeAllThreads(newThreads);
this._setState({ ...this.state, allThreads: newThreads }, true)
this._setState({ ...this.state, allThreads: newThreads })
}
duplicateThread(threadId: string) {
@@ -1529,7 +1627,7 @@ We only need to do it for files that were edited since `from`, ie files between
[newThread.id]: newThread,
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true)
this._setState({ allThreads: newThreads })
}
@@ -1550,7 +1648,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
this._setState({ allThreads: newThreads }) // 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)
@@ -1571,7 +1669,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}
}, true)
})
// // when change focused message idx, jump - do not jump back when click edit, too confusing.
// if (messageIdx !== undefined)
@@ -1611,6 +1709,31 @@ We only need to do it for files that were edited since `from`, ie files between
}
// Pops the staging selections from the current thread's state
popStagingSelections(numPops: number): void {
numPops = numPops ?? 1;
const focusedMessageIdx = this.getCurrentFocusedMessageIdx()
// set the selections to the proper value
let selections: StagingSelectionItem[] = []
let setSelections = (s: StagingSelectionItem[]) => { }
if (focusedMessageIdx === undefined) {
selections = this.getCurrentThreadState().stagingSelections
setSelections = (s: StagingSelectionItem[]) => this.setCurrentThreadState({ stagingSelections: s })
} else {
selections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections
setSelections = (s) => this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
}
setSelections([
...selections.slice(0, selections.length - numPops)
])
}
// set message.state
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
@@ -1634,12 +1757,12 @@ We only need to do it for files that were edited since `from`, ie files between
)
}
}
}, true)
})
}
// set thread.state
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>): void {
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>, doNotRefreshMountInfo?: boolean): void {
const thread = this.state.allThreads[threadId]
if (!thread) return
@@ -1654,7 +1777,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}
}, true)
}, doNotRefreshMountInfo)
}
@@ -6,17 +6,18 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { URI } from '../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../editor/common/model.js';
export const EMPTY_MESSAGE = '(empty message)'
@@ -37,11 +38,8 @@ type SimpleLLMMessage = {
const EMPTY_MESSAGE = '(empty message)'
const CHARS_PER_TOKEN = 4
const TRIM_TO_LEN = 60
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
const TRIM_TO_LEN = 120
@@ -69,7 +67,7 @@ openai on developer system message - https://cdn.openai.com/spec/model-spec-2024
*/
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): AnthropicOrOpenAILLMMessage[] => {
const newMessages: OpenAILLMChatMessage[] = [];
@@ -136,8 +134,9 @@ assistant: ...content, call(name, id, params)
user: ...content, result(id, content)
*/
type AnthropicOrOpenAILLMMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
for (let i = 0; i < messages.length; i += 1) {
@@ -195,9 +194,9 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA
}
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
const llmChatMessages: LLMChatMessage[] = [];
const llmChatMessages: AnthropicOrOpenAILLMMessage[] = [];
for (let i = 0; i < messages.length; i += 1) {
const c = messages[i]
@@ -206,7 +205,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
if (c.role === 'assistant') {
// if called a tool (message after it), re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content: LLMChatMessage['content'] = c.content
let content: AnthropicOrOpenAILLMMessage['content'] = c.content
if (next?.role === 'tool') {
content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}`
}
@@ -238,33 +237,17 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
}
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = []
if (!specialToolFormat) { // XML tool behavior
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'anthropic-style') {
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'openai-style') {
return prepareMessages_openai_tools(messages)
}
return llmChatMessages
}
// --- CHAT ---
const prepareMessages = ({
messages,
const prepareOpenAIOrAnthropicMessages = ({
messages: messages_,
systemMessage,
aiInstructions,
supportsSystemMessage,
specialToolFormat,
supportsAnthropicReasoning,
contextWindow,
maxOutputTokens,
reservedOutputTokenSpace,
}: {
messages: SimpleLLMMessage[],
systemMessage: string,
@@ -273,20 +256,35 @@ const prepareMessages = ({
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
supportsAnthropicReasoning: boolean,
contextWindow: number,
maxOutputTokens: number | null | undefined,
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
reservedOutputTokenSpace: number | null | undefined,
}): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => {
reservedOutputTokenSpace = Math.max(
contextWindow * 1 / 2, // reserve at least 1/4 of the token window length
reservedOutputTokenSpace ?? 4_096 // defaults to 4096
)
let messages: (SimpleLLMMessage | { role: 'system', content: string })[] = deepClone(messages_)
// ================ system message ================
// A COMPLETE HACK: last message is system message for context purposes
const sysMsgParts: string[] = []
if (aiInstructions) sysMsgParts.push(`GUIDELINES (from the user's .voidrules file):\n${aiInstructions}`)
if (systemMessage) sysMsgParts.push(systemMessage)
const combinedSystemMessage = sysMsgParts.join('\n\n')
messages.unshift({ role: 'system', content: combinedSystemMessage })
// ================ trim ================
messages = deepClone(messages)
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
type MesType = (typeof messages)[0]
// ================ fit into context ================
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
const alreadyTrimmedIdxes = new Set<number>()
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
const weight = (message: MesType, messages: MesType[], idx: number) => {
const base = message.content.length
let multiplier: number
@@ -294,22 +292,30 @@ const prepareMessages = ({
if (message.role === 'user') {
multiplier *= 1
}
else if (message.role === 'system') {
multiplier *= .01 // very low weight
}
else {
multiplier *= 10 // llm tokens are far less valuable than user tokens
}
// 1st message, last 3 msgs, any already modified message should be low in weight
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
// any already modified message should not be trimmed again
if (alreadyTrimmedIdxes.has(idx)) {
multiplier = 0
}
// 1st and last messages should be very low weight
if (idx <= 1 || idx >= messages.length - 1 - 3) {
multiplier *= .05
}
return base * multiplier
}
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
const _findLargestByWeight = (messages_: MesType[]) => {
let largestIndex = -1
let largestWeight = -Infinity
for (let i = 0; i < messages.length; i += 1) {
const m = messages[i]
const w = weight(m, messages, i)
const w = weight(m, messages_, i)
if (w > largestWeight) {
largestWeight = w
largestIndex = i
@@ -320,7 +326,11 @@ const prepareMessages = ({
let totalLen = 0
for (const m of messages) { totalLen += m.content.length }
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
const charsNeedToTrim = totalLen - Math.max(
(contextWindow - reservedOutputTokenSpace) * CHARS_PER_TOKEN, // can be 0, in which case charsNeedToTrim=everything, bad
5_000 // ensure we don't trim at least 5k chars (just a random small value)
)
// <----------------------------------------->
// 0 | | |
@@ -340,41 +350,54 @@ const prepareMessages = ({
// if can finish here, do
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
if (numCharsWillTrim > remainingCharsToTrim) {
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
// trim remainingCharsToTrim + '...'.length chars
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim - '...'.length).trim() + '...'
break
}
remainingCharsToTrim -= numCharsWillTrim
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
m.content = m.content.substring(0, TRIM_TO_LEN - '...'.length) + '...'
alreadyTrimmedIdxes.add(trimIdx)
}
// ================ system message hack ================
const newSysMsg = messages.shift()!.content
// ================ tools and anthropicReasoning ================
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
// SYSTEM MESSAGE HACK: we shifted (removed) the system message role, so now SimpleLLMMessage[] is valid
// ================ system message concat ================
let llmChatMessages: AnthropicOrOpenAILLMMessage[] = []
if (!specialToolFormat) { // XML tool behavior
llmChatMessages = prepareMessages_XML_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
}
else if (specialToolFormat === 'anthropic-style') {
llmChatMessages = prepareMessages_anthropic_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
}
else if (specialToolFormat === 'openai-style') {
llmChatMessages = prepareMessages_openai_tools(messages as SimpleLLMMessage[])
}
const llmMessages = llmChatMessages
// find system messages and concatenate them
const newSystemMessage = aiInstructions ?
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
: systemMessage
// ================ system message add as first llmMessage ================
let separateSystemMessageStr: string | undefined = undefined
// if supports system message
if (supportsSystemMessage) {
if (supportsSystemMessage === 'separated')
separateSystemMessageStr = newSystemMessage
separateSystemMessageStr = newSysMsg
else if (supportsSystemMessage === 'system-role')
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
llmMessages.unshift({ role: 'system', content: newSysMsg }) // add new first message
else if (supportsSystemMessage === 'developer-role')
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
llmMessages.unshift({ role: 'developer', content: newSysMsg }) // add new first message
}
// if does not support system message
else {
const newFirstMessage = {
role: 'user',
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
content: `<SYSTEM_MESSAGE>\n${newSysMsg}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
} as const
llmMessages.splice(0, 1) // delete first message
llmMessages.unshift(newFirstMessage) // add new first message
@@ -382,14 +405,25 @@ const prepareMessages = ({
// ================ no empty message ================
for (const currMsg of llmMessages) {
for (let i = 0; i < llmMessages.length; i += 1) {
const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i]
const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1]
if (currMsg.role === 'tool') continue
// if content is a string, replace string with empty msg
if (typeof currMsg.content === 'string')
if (typeof currMsg.content === 'string') {
currMsg.content = currMsg.content || EMPTY_MESSAGE
}
else {
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
// allowed to be empty if has a tool in it or following it
if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) {
currMsg.content = currMsg.content.filter(c => !(c.type === 'text' && !c.text)) as any
continue
}
if (nextMsg?.role === 'tool') continue
// replace any empty text entries with empty msg, and make sure there's at least 1 entry
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
}
@@ -406,9 +440,79 @@ const prepareMessages = ({
type GeminiUserPart = (GeminiLLMChatMessage & { role: 'user' })['parts'][0]
type GeminiModelPart = (GeminiLLMChatMessage & { role: 'model' })['parts'][0]
const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
let latestToolName: ToolName | undefined = undefined
const messages2: GeminiLLMChatMessage[] = messages.map((m): GeminiLLMChatMessage | null => {
if (m.role === 'assistant') {
if (typeof m.content === 'string') {
return { role: 'model', parts: [{ text: m.content }] }
}
else {
const parts: GeminiModelPart[] = m.content.map((c): GeminiModelPart | null => {
if (c.type === 'text') {
return { text: c.text }
}
else if (c.type === 'tool_use') {
latestToolName = c.name as ToolName
return { functionCall: { id: c.id, name: c.name as ToolName, args: c.input } }
}
else return null
}).filter(m => !!m)
return { role: 'model', parts, }
}
}
else if (m.role === 'user') {
if (typeof m.content === 'string') {
return { role: 'user', parts: [{ text: m.content }] } satisfies GeminiLLMChatMessage
}
else {
const parts: GeminiUserPart[] = m.content.map((c): GeminiUserPart | null => {
if (c.type === 'text') {
return { text: c.text }
}
else if (c.type === 'tool_result') {
if (!latestToolName) return null
return { functionResponse: { id: c.tool_use_id, name: latestToolName, response: { output: c.content } } }
}
else return null
}).filter(m => !!m)
return { role: 'user', parts, }
}
}
else return null
}).filter(m => !!m)
return messages2
}
const prepareMessages = (params: {
messages: SimpleLLMMessage[],
systemMessage: string,
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined,
supportsAnthropicReasoning: boolean,
contextWindow: number,
reservedOutputTokenSpace: number | null | undefined,
providerName: ProviderName
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
const specialFormat = params.specialToolFormat // this is just for ts stupidness
// if need to convert to gemini style of messaes, do that (treat as anthropic style, then convert to gemini style)
if (params.providerName === 'gemini' || specialFormat === 'gemini-style') {
const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined })
const messages = res.messages as AnthropicLLMChatMessage[]
const messages2 = prepareGeminiMessages(messages)
return { messages: messages2, separateSystemMessage: res.separateSystemMessage }
}
return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat })
}
@@ -418,7 +522,6 @@ export interface IConvertToLLMMessageService {
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] }
}
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
@@ -470,7 +573,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
// system message
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined) => {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
@@ -481,6 +584,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
`...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const includeXMLToolDefinitions = !specialToolFormat
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
@@ -527,20 +631,23 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { overridesOfModel } = this.voidSettingsService.state
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName)
} = getModelCapabilities(providerName, modelName, overridesOfModel)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel)
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
const { messages, separateSystemMessage } = prepareMessages({
messages: simpleMessages,
@@ -550,18 +657,22 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
maxOutputTokens,
reservedOutputTokenSpace,
providerName,
})
return { messages, separateSystemMessage };
}
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { overridesOfModel } = this.voidSettingsService.state
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName)
} = getModelCapabilities(providerName, modelName, overridesOfModel)
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
@@ -569,8 +680,8 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel)
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
const { messages, separateSystemMessage } = prepareMessages({
@@ -581,7 +692,8 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
maxOutputTokens,
reservedOutputTokenSpace,
providerName,
})
return { messages, separateSystemMessage };
}
@@ -14,8 +14,6 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit
import { findDiffs } from './helpers/findDiffs.js';
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
import { Color, RGBA } from '../../../../base/common/color.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js';
import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
@@ -25,7 +23,10 @@ import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, } from '../common/prompt/prompts.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, tripleTick, } from '../common/prompt/prompts.js';
import { IVoidCommandBarService } from './voidCommandBarService.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID } from './actionIDs.js';
import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
import { QuickEditPropsType } from './quickEditActions.js';
@@ -48,27 +49,6 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { isMacintosh } from '../../../../base/common/platform.js';
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
}
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
const greenBG = new Color(new RGBA(155, 185, 85, .2)); // default is RGBA(155, 185, 85, .2)
registerColor('void.greenBG', configOfBG(greenBG), '', true);
const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2)
registerColor('void.redBG', configOfBG(redBG), '', true);
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
const numLinesOfStr = (str: string) => str.split('\n').length
@@ -129,29 +109,42 @@ const removeWhitespaceExceptNewlines = (str: string): string => {
// finds block.orig in fileContents and return its range in file
// startingAtLine is 1-indexed and inclusive
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
// returns 1-indexed lines
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' }) => {
const startLineIdx = (fileContents: string) => startingAtLine !== undefined ?
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
const returnAns = (fileContents: string, idx: number) => {
const startLine = numLinesOfStr(fileContents.substring(0, idx + 1))
const numLines = numLinesOfStr(text)
const endLine = startLine + numLines - 1
return [startLine, endLine] as const
}
const startingAtLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ?
fileContents.split('\n').slice(0, opts.startingAtLine).join('\n').length // num characters in all lines before startingAtLine
: 0
// idx = starting index in fileContents
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
let idx = fileContents.indexOf(text, startingAtLineIdx(fileContents))
// if idx was found
if (idx !== -1) {
return returnAns(fileContents, idx)
}
if (!canFallbackToRemoveWhitespace)
return 'Not found' as const
// try to find it ignoring all whitespace this time
if (idx === -1 && canFallbackToRemoveWhitespace) {
text = removeWhitespaceExceptNewlines(text)
fileContents = removeWhitespaceExceptNewlines(fileContents)
idx = fileContents.indexOf(text, startLineIdx(fileContents));
}
text = removeWhitespaceExceptNewlines(text)
fileContents = removeWhitespaceExceptNewlines(fileContents)
idx = fileContents.indexOf(text, startingAtLineIdx(fileContents));
if (idx === -1) return 'Not found' as const
const lastIdx = fileContents.lastIndexOf(text)
if (lastIdx !== idx) return 'Not unique' as const
const startLine = fileContents.substring(0, idx).split('\n').length
const numLines = numLinesOfStr(text)
const endLine = startLine + numLines - 1
return [startLine, endLine] as const
return returnAns(fileContents, idx)
}
@@ -276,7 +269,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
public processRawKeybindingText(keybindingStr: string): string {
return keybindingStr
.replace(/Enter/g, '↵') // ⏎
.replace(/Backspace/g, '⌫');
}
// private _notifyError = (e: Parameters<OnError>[0]) => {
// const details = errorDetails(e.fullError)
@@ -590,7 +587,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
else { throw new Error('Void 1') }
const buttonsWidget = new AcceptRejectInlineWidget({
const buttonsWidget = this._instantiationService.createInstance(AcceptRejectInlineWidget, {
editor,
onAccept: () => {
this.acceptDiff({ diffid })
@@ -1127,8 +1124,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
return
}
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
const uri = this._getURIBeforeStartApplying(opts)
public async callBeforeApplyOrEdit(givenURI: URI | 'current') {
const uri = this._uriOfGivenURI(givenURI)
if (!uri) return
await this._voidModelService.initializeModel(uri)
await this._voidModelService.saveModel(uri) // save the URI
@@ -1185,14 +1182,25 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
const onError = (e: { message: string; fullError: Error | null; }) => {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message)
}
try {
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
}
catch (e) {
onError({ message: e + '', fullError: null })
}
onDone()
}
public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) {
public instantlyRewriteFile({ uri, newContent }: { uri: URI, newContent: string }) {
// start diffzone
const res = this._startStreamingDiffZone({
uri,
@@ -1212,7 +1220,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
onFinishEdit()
}
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false })
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: true })
onDone()
}
@@ -1335,6 +1343,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { from, } = opts
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -1446,7 +1455,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError
throw e.fullError || new Error(e.message)
}
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
@@ -1486,6 +1495,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
@@ -1560,25 +1570,31 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
const descStr = str === `Not found` ?
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === `Not unique` ?
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === 'Has overlap' ?
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: ``
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
const errMsg = `${descStr}\n${soFarStr}`
return errMsg
/**
* Generates a human-readable error message for an invalid ORIGINAL search block.
*/
private _errContentOfInvalidStr = (
str: 'Not found' | 'Not unique' | 'Has overlap',
blockOrig: string,
): string => {
const problematicCode = `${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}`
// use a switch for better readability / exhaustiveness check
let descStr: string
switch (str) {
case 'Not found':
descStr = `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.`
break
case 'Not unique':
descStr = `The edit was not applied. The text in ORIGINAL must be unique in the file being edited, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.`
break
case 'Has overlap':
descStr = `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.`
break
default:
descStr = ''
}
return descStr
}
@@ -1589,23 +1605,31 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { model } = this._voidModelService.getModel(uri)
if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`)
const modelStr = model.getValue(EndOfLinePreference.LF)
// .split('\n').map(l => '\t' + l).join('\n') // for testing purposes only, remember to remove this
const modelStrLines = modelStr.split('\n')
const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = []
for (const b of blocks) {
const i = modelStr.indexOf(b.orig)
if (i === -1)
throw new Error(this._errContentOfInvalidStr('Not found', b.orig))
const j = modelStr.lastIndexOf(b.orig)
if (i !== j)
throw new Error(this._errContentOfInvalidStr('Not unique', b.orig))
const res = findTextInCode(b.orig, modelStr, true, { returnType: 'lines' })
if (typeof res === 'string')
throw new Error(this._errContentOfInvalidStr(res, b.orig))
let [startLine, endLine] = res
startLine -= 1 // 0-index
endLine -= 1
replacements.push({
origStart: i,
origEnd: i + b.orig.length - 1, // INCLUSIVE
block: b,
})
// including newline before start
const origStart = (startLine !== 0 ?
modelStrLines.slice(0, startLine).join('\n') + '\n'
: '').length
// including endline at end
const origEnd = modelStrLines.slice(0, endLine + 1).join('\n').length - 1
replacements.push({ origStart, origEnd, block: b });
}
// sort in increasing order
replacements.sort((a, b) => a.origStart - b.origStart)
@@ -1627,12 +1651,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
'wholeFileRange',
{ shouldRealignDiffAreas: true }
)
}
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
const { from, applyStr, } = opts
const featureName: FeatureName = 'Apply'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -1714,7 +1738,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message) // throw error h
throw e.fullError || new Error(e.message)
}
// refresh now in case onText takes a while to get 1st message
@@ -1768,7 +1792,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// update stream state to the first line of original if some portion of original has been written
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
const originalRange = findTextInCode(block.orig, originalFileCode, false, { startingAtLine, returnType: 'lines' })
if (typeof originalRange !== 'string') {
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
diffZone._streamState.line = startLine
@@ -1794,7 +1818,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
const originalBounds = findTextInCode(block.orig, originalFileCode, true, { returnType: 'lines' })
// if error
// Check for overlap with existing modified ranges
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
@@ -1813,9 +1837,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('block.orig:', block.orig)
console.log('---------')
const content = this._errContentOfInvalidStr(errorMessage, block.orig)
const retryMsg = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
messages.push(
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong
{ role: 'user', content: content + '\n' + retryMsg } // user explanation of what's wrong
)
// REVERT ALL BLOCKS
@@ -1911,6 +1936,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
@@ -1924,6 +1950,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (blocks.length === 0) {
this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`)
}
this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true })
try {
this._instantlyApplySRBlocks(uri, fullText)
@@ -2231,31 +2259,77 @@ registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager);
class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
public getId() { return this.ID }
public getDomNode() { return this._domNode; }
public getPosition() { return null }
public getId(): string {
return this.ID || ''; // Ensure we always return a string
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition() {
return null;
}
private readonly _domNode: HTMLElement;
private readonly editor
private readonly ID
private readonly startLine
private readonly _domNode: HTMLElement; // Using the definite assignment assertion
private readonly editor: ICodeEditor;
private readonly ID: string;
private readonly startLine: number;
constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) {
super()
constructor(
{ editor, onAccept, onReject, diffid, startLine, offsetLines }: {
editor: ICodeEditor;
onAccept: () => void;
onReject: () => void;
diffid: string,
startLine: number,
offsetLines: number
},
@IVoidCommandBarService private readonly _voidCommandBarService: IVoidCommandBarService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
) {
super();
this.ID = editor.getModel()?.uri.fsPath + diffid;
const uri = editor.getModel()?.uri;
// Initialize with default values
this.ID = ''
this.editor = editor;
this.startLine = startLine;
if (!uri) {
const { dummyDiv } = dom.h('div@dummyDiv');
this._domNode = dummyDiv
return;
}
this.ID = uri.fsPath + diffid;
const lineHeight = editor.getOption(EditorOption.lineHeight);
const getAcceptRejectText = () => {
const acceptKeybinding = this._keybindingService.lookupKeybinding(VOID_ACCEPT_DIFF_ACTION_ID);
const rejectKeybinding = this._keybindingService.lookupKeybinding(VOID_REJECT_DIFF_ACTION_ID);
// Use the standalone function directly since we're in a nested class that
// can't access EditCodeService's methods
const acceptKeybindLabel = this._editCodeService.processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || '');
const rejectKeybindLabel = this._editCodeService.processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '');
const commandBarStateAtUri = this._voidCommandBarService.stateOfURI[uri.fsPath];
const selectedDiffIdx = commandBarStateAtUri?.diffIdx ?? 0; // 0th item is selected by default
const thisDiffIdx = commandBarStateAtUri?.sortedDiffIds.indexOf(diffid) ?? null;
const showLabel = thisDiffIdx === selectedDiffIdx
const acceptText = `Accept${showLabel ? ` ` + acceptKeybindLabel : ''}`;
const rejectText = `Reject${showLabel ? ` ` + rejectKeybindLabel : ''}`;
return { acceptText, rejectText }
}
const { acceptText, rejectText } = getAcceptRejectText()
// Create container div with buttons
const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
dom.h('button@acceptButton', []),
@@ -2269,11 +2343,14 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
buttons.style.paddingRight = '4px';
buttons.style.zIndex = '1';
buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`;
buttons.style.justifyContent = 'flex-end';
buttons.style.width = '100%';
buttons.style.pointerEvents = 'none';
// Style accept button
acceptButton.onclick = onAccept;
acceptButton.textContent = 'Accept';
acceptButton.textContent = acceptText;
acceptButton.style.backgroundColor = acceptBg;
acceptButton.style.border = acceptBorder;
acceptButton.style.color = buttonTextColor;
@@ -2287,10 +2364,12 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
acceptButton.style.cursor = 'pointer';
acceptButton.style.height = '100%';
acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
acceptButton.style.pointerEvents = 'auto';
// Style reject button
rejectButton.onclick = onReject;
rejectButton.textContent = 'Reject';
rejectButton.textContent = rejectText;
rejectButton.style.backgroundColor = rejectBg;
rejectButton.style.border = rejectBorder;
rejectButton.style.color = buttonTextColor;
@@ -2304,6 +2383,7 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
rejectButton.style.cursor = 'pointer';
rejectButton.style.height = '100%';
rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
rejectButton.style.pointerEvents = 'auto';
@@ -2324,16 +2404,28 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
}
// Mount first, then update positions
editor.addOverlayWidget(this);
updateTop()
updateLeft()
setTimeout(() => {
updateTop()
updateLeft()
}, 0)
this._register(editor.onDidScrollChange(e => { updateTop() }))
this._register(editor.onDidChangeModelContent(e => { updateTop() }))
this._register(editor.onDidLayoutChange(e => { updateTop(); updateLeft() }))
// Listen for state changes in the command bar service
this._register(this._voidCommandBarService.onDidChangeState(e => {
if (uri && e.uri.fsPath === uri.fsPath) {
const { acceptText, rejectText } = getAcceptRejectText()
acceptButton.textContent = acceptText;
rejectButton.textContent = rejectText;
}
}));
// mount this widget
editor.addOverlayWidget(this);
@@ -2341,8 +2433,8 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
}
public override dispose(): void {
this.editor.removeOverlayWidget(this)
super.dispose()
this.editor.removeOverlayWidget(this);
super.dispose();
}
}
@@ -42,10 +42,12 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
export interface IEditCodeService {
readonly _serviceBrand: undefined;
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
processRawKeybindingText(keybindingStr: string): string;
callBeforeApplyOrEdit(uri: URI | 'current'): Promise<void>;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void;
instantlyApplyNewContent(opts: { uri: URI; newContent: string }): void;
instantlyRewriteFile(opts: { uri: URI; newContent: string }): void;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
@@ -54,6 +56,8 @@ export interface IEditCodeService {
diffOfId: Record<string, Diff>;
acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void;
acceptDiff({ diffid }: { diffid: number }): void;
rejectDiff({ diffid }: { diffid: number }): void;
// events
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
@@ -0,0 +1,329 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../../base/common/buffer.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { env } from '../../../../base/common/process.js';
import { URI } from '../../../../base/common/uri.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { TransferEditorType, TransferFilesInfo } from './extensionTransferTypes.js';
export interface IExtensionTransferService {
readonly _serviceBrand: undefined; // services need this, just leave it undefined
transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType): Promise<string | undefined>
deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null): Promise<void>
}
export const IExtensionTransferService = createDecorator<IExtensionTransferService>('ExtensionTransferService');
// Define extensions to skip when transferring
const extensionBlacklist = [
// ignore extensions
'ms-vscode-remote.remote', // ms-vscode-remote.remote-ssh, ms-vscode-remote.remote-wsl
'ms-vscode.remote', // ms-vscode.remote-explorer
// ignore other AI copilots that could conflict with Void keybindings
'sourcegraph.cody-ai',
'continue.continue',
'codeium.codeium',
'saoudrizwan.claude-dev', // cline
'rooveterinaryinc.roo-cline', // roo
'supermaven.supermaven' // supermaven
// 'github.copilot',
];
const isBlacklisted = (fsPath: string | undefined) => {
return extensionBlacklist.find(bItem => fsPath?.includes(bItem))
}
class ExtensionTransferService extends Disposable implements IExtensionTransferService {
_serviceBrand: undefined;
constructor(
@IFileService private readonly _fileService: IFileService,
) {
super()
}
async transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType) {
const transferTheseFiles = transferTheseFilesOfOS(os, fromEditor)
const fileService = this._fileService
let errAcc = ''
for (const { from, to, isExtensions } of transferTheseFiles) {
// Check if the source file exists before attempting to copy
try {
if (!isExtensions) {
console.log('transferring item', from, to)
const exists = await fileService.exists(from)
if (exists) {
// Ensure the destination directory exists
const toParent = URI.joinPath(to, '..')
const toParentExists = await fileService.exists(toParent)
if (!toParentExists) {
await fileService.createFolder(toParent)
}
await fileService.copy(from, to, true)
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
}
// extensions folder
else {
console.log('transferring extensions...', from, to)
const exists = await fileService.exists(from)
if (exists) {
const stat = await fileService.resolve(from)
const toParent = URI.joinPath(to) // extensions/
const toParentExists = await fileService.exists(toParent)
if (!toParentExists) {
await fileService.createFolder(toParent)
}
for (const extensionFolder of stat.children ?? []) {
const from = extensionFolder.resource
const to = URI.joinPath(toParent, extensionFolder.name)
const toStat = await fileService.resolve(from)
if (toStat.isDirectory) {
if (!isBlacklisted(extensionFolder.resource.fsPath)) {
await fileService.copy(from, to, true)
}
}
else if (toStat.isFile) {
if (extensionFolder.name === 'extensions.json') {
try {
const contentsStr = await fileService.readFile(from)
const json: any = JSON.parse(contentsStr.value.toString())
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
const jsonStr = JSON.stringify(j2)
await fileService.writeFile(to, VSBuffer.fromString(jsonStr))
}
catch {
console.log('Error copying extensions.json, skipping')
}
}
}
}
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
console.log('done transferring extensions.')
}
}
catch (e) {
console.error('Error copying file:', e)
errAcc += `Error copying ${from.toString()}: ${e}\n`
}
}
if (errAcc) return errAcc
return undefined
}
async deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null) {
const fileService = this._fileService
const extensionsURI = getExtensionsFolder(os)
if (!extensionsURI) return
const eURI = await fileService.resolve(extensionsURI)
for (const child of eURI.children ?? []) {
try {
if (child.isDirectory) {
// if is blacklisted
if (isBlacklisted(child.resource.fsPath)) {
console.log('Deleting extension', child.resource.fsPath)
await fileService.del(child.resource, { recursive: true, useTrash: true })
}
}
else if (child.isFile) {
// if is extensions.json
if (child.name === 'extensions.json') {
console.log('Updating extensions.json', child.resource.fsPath)
try {
const contentsStr = await fileService.readFile(child.resource)
const json: any = JSON.parse(contentsStr.value.toString())
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
const jsonStr = JSON.stringify(j2)
await fileService.writeFile(child.resource, VSBuffer.fromString(jsonStr))
}
catch {
console.log('Error copying extensions.json, skipping')
}
}
}
}
catch (e) {
console.error('Could not delete extension', child.resource.fsPath, e)
}
}
}
}
registerSingleton(IExtensionTransferService, ExtensionTransferService, InstantiationType.Eager); // lazily loaded, even if Eager
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => {
if (os === null)
throw new Error(`One-click switch is not possible in this environment.`)
if (os === 'mac') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`$HOME not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
if (os === 'linux') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`variable for $HOME location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
if (os === 'windows') {
const appdata = env['APPDATA']
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
const userprofile = env['USERPROFILE']
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`)
}
const getExtensionsFolder = (os: 'mac' | 'windows' | 'linux' | null) => {
const t = transferTheseFilesOfOS(os, 'VS Code') // from editor doesnt matter
return t.find(f => f.isExtensions)?.to
}
@@ -0,0 +1,11 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js'
export type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
export type TransferFilesInfo = { from: URI, to: URI, isExtensions?: boolean }[]
@@ -0,0 +1,78 @@
import { localize2 } from '../../../../nls.js';
import { URI } from '../../../../base/common/uri.js';
import { Action2, registerAction2, MenuId } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { messageOfSelection } from '../common/prompt/prompts.js';
import { IVoidModelService } from '../common/voidModelService.js';
class FilePromptActionService extends Action2 {
private static readonly VOID_COPY_FILE_PROMPT_ID = 'void.copyfileprompt'
constructor() {
super({
id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID,
title: localize2('voidCopyPrompt', 'Void: Copy Prompt'),
menu: [{
id: MenuId.ExplorerContext,
group: '8_void',
order: 1,
}]
});
}
async run(accessor: ServicesAccessor, uri: URI): Promise<void> {
try {
const fileService = accessor.get(IFileService);
const clipboardService = accessor.get(IClipboardService)
const directoryStrService = accessor.get(IDirectoryStrService)
const voidModelService = accessor.get(IVoidModelService)
const stat = await fileService.stat(uri)
const folderOpts = {
maxChildren: 1000,
maxCharsPerFile: 2_000_000,
} as const
let m: string = 'No contents detected'
if (stat.isFile) {
m = await messageOfSelection({
type: 'File',
uri,
language: (await voidModelService.getModelSafe(uri)).model?.getLanguageId() || '',
state: { wasAddedAsCurrentFile: false, },
}, {
folderOpts,
directoryStrService,
fileService,
})
}
if (stat.isDirectory) {
m = await messageOfSelection({
type: 'Folder',
uri,
}, {
folderOpts,
fileService,
directoryStrService,
})
}
await clipboardService.writeText(m)
} catch (error) {
const notificationService = accessor.get(INotificationService)
notificationService.error(error + '')
}
}
}
registerAction2(FilePromptActionService)
@@ -0,0 +1,49 @@
/*--------------------------------------------------------------------------------------
* 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 { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { IExtensionTransferService } from './extensionTransferService.js';
import { os } from '../common/helpers/systemInfo.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { timeout } from '../../../../base/common/async.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
// Onboarding contribution that mounts the component at startup
export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.voidMiscWorkbenchContribs';
constructor(
@IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.initialize();
}
private initialize(): void {
// delete blacklisted extensions once (this is for people who already installed them)
const deleteExtensionsStorageId = 'void-deleted-blacklist-2'
const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
if (!alreadyDeleted) {
this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
this.extensionTransferService.deleteBlacklistExtensions(os)
}
// after some time, trigger a resize event for the blank screen error
timeout(5_000).then(() => {
// Get the active window reference for multi-window support
const targetWindow = getActiveWindow();
// Trigger a window resize event to ensure proper layout calculations
targetWindow.dispatchEvent(new Event('resize'))
})
}
}
registerWorkbenchContribution2(MiscWorkbenchContribs.ID, MiscWorkbenchContribs, WorkbenchPhase.Eventually);
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { useState, useEffect, useCallback, useRef, Fragment } from 'react'
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { usePromise, useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { getBasename, ListableToolItem, voidOpenFileFn, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { PlacesType, VariantType } from 'react-tooltip'
enum CopyButtonText {
@@ -24,8 +24,9 @@ type IconButtonProps = {
Icon: LucideIcon
}
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
@@ -34,19 +35,19 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
}}
// border border-void-border-1 rounded
className={`
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
{...props}
>
<Icon />
</button>
)
}
// export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
@@ -108,7 +109,7 @@ export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & R
<IconShell1
Icon={FileSymlink}
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
voidOpenFileFn(uri, accessor)
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
{...props}
@@ -131,48 +132,41 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
// state persisted for duration of react only
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
const _applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
const getUriBeingApplied = (applyBoxId: string) => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
return _applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
export const useApplyStreamState = ({ applyBoxId }: { applyBoxId: string }) => {
const accessor = useAccessor()
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const [_, rerender] = useState(0)
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, applyBoxId])
const [currStreamStateRef, setStreamState] = useRefState(getStreamState())
const setApplying = useCallback((uri: URI | undefined) => {
_applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri ?? undefined
setStreamState(getStreamState())
}, [setStreamState, getStreamState, applyBoxId])
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = (
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
)
if (shouldUpdate) {
rerender(c => c + 1)
const uri = getUriBeingApplied(applyBoxId)
if (uri?.fsPath === uri_.fsPath) {
setStreamState(getStreamState())
}
}, [applyBoxId, uri]))
const currStreamState = getStreamState()
}, [setStreamState, applyBoxId, getStreamState]))
return {
getStreamState,
isDisabled,
currStreamState,
}
return { currStreamStateRef, setApplying }
}
@@ -202,10 +196,24 @@ const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position =
'data-tooltip-offset': offset,
})
export const useEditToolStreamState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI }) => {
const accessor = useAccessor()
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const [streamState, setStreamState] = useState(voidCommandBarService.getStreamState(uri))
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = uri.fsPath === uri_.fsPath
if (shouldUpdate) { setStreamState(voidCommandBarService.getStreamState(uri)) }
}, [voidCommandBarService, applyBoxId, uri]))
return { streamState, }
}
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
const currStreamState = currStreamStateRef.current
const color = (
currStreamState === 'idle-no-changes' ? 'dark' :
@@ -231,82 +239,255 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
}
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
const terminalLanguages = new Set([
'bash',
'shellscript',
'shell',
'powershell',
'bat',
'zsh',
'sh',
'fish',
'nushell',
'ksh',
'xonsh',
'elvish',
])
const ApplyButtonsForTerminal = ({
codeStr,
applyBoxId,
uri,
language,
}: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const accessor = useAccessor()
const metricsService = accessor.get('IMetricsService')
const terminalToolService = accessor.get('ITerminalToolService')
const settingsState = useSettingsState()
const [isShellRunning, setIsShellRunning] = useState<boolean>(false)
const interruptToolRef = useRef<(() => void) | null>(null)
const isDisabled = isShellRunning
const onClickSubmit = useCallback(async () => {
if (isShellRunning) return
try {
setIsShellRunning(true)
const terminalId = await terminalToolService.createPersistentTerminal({ cwd: null })
const { interrupt } = await terminalToolService.runCommand(
codeStr,
{ type: 'persistent', persistentTerminalId: terminalId }
);
interruptToolRef.current = interrupt
metricsService.capture('Execute Shell', { length: codeStr.length })
} catch (e) {
setIsShellRunning(false)
console.error('Failed to execute in terminal:', e)
}
}, [codeStr, uri, applyBoxId, metricsService, terminalToolService, isShellRunning])
if (isShellRunning) {
return (
<IconShell1
Icon={X}
onClick={() => {
interruptToolRef.current?.();
setIsShellRunning(false);
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
);
}
if (isDisabled) {
return null
}
return <IconShell1
Icon={Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
const ApplyButtonsForEdit = ({
codeStr,
applyBoxId,
uri,
language,
}: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const notificationService = accessor.get('INotificationService')
const {
currStreamState,
isDisabled,
getStreamState,
} = useApplyButtonState({ applyBoxId, uri })
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId })
const onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (getStreamState() === 'streaming') return
const opts = {
if (currStreamStateRef.current === 'streaming') return
await editCodeService.callBeforeApplyOrEdit(uri)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying({
from: 'ClickApply',
applyStr: codeStr,
uri: uri,
startBehavior: 'reject-conflicts',
} as const
}) ?? []
setApplying(newApplyingUri)
await editCodeService.callBeforeStartApplying(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
if (!applyDonePromise) {
notificationService.info(`Void Error: We couldn't run Apply here. ${uri === 'current' ? 'This Apply block wants to run on the current file, but you might not have a file open.' : `This Apply block wants to run on ${uri.fsPath}, but it might not exist.`}`)
}
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
notificationService.info(`Void Error: There was a problem running Apply: ${e}.`)
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
// rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService, notificationService])
const onInterrupt = useCallback(() => {
if (getStreamState() !== 'streaming') return
const onClickStop = useCallback(() => {
if (currStreamStateRef.current !== 'streaming') return
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [getStreamState, applyBoxId, editCodeService, metricsService])
}, [currStreamStateRef, applyBoxId, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'accept', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const currStreamState = currStreamStateRef.current
if (currStreamState === 'streaming') {
return <IconShell1
Icon={Square}
onClick={onInterrupt}
onClick={onClickStop}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
}
if (isDisabled) {
return null
}
if (currStreamState === 'idle-no-changes') {
return <IconShell1
Icon={Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
if (currStreamState === 'idle-has-changes') {
return <Fragment>
<IconShell1
Icon={X}
onClick={onReject}
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
/>
</Fragment>
}
}
export const ApplyButtonsHTML = (params: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const { language } = params
const isShellLanguage = !!language && terminalLanguages.has(language)
if (isShellLanguage) {
return <ApplyButtonsForTerminal {...params} />
}
else {
return <ApplyButtonsForEdit {...params} />
}
}
export const EditToolAcceptRejectButtonsHTML = ({
codeStr,
applyBoxId,
uri,
type,
threadId,
}: {
codeStr: string,
applyBoxId: string,
} & ({
uri: URI,
type: 'edit_file' | 'rewrite_file',
threadId: string,
})
) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const { streamState } = useEditToolStreamState({ applyBoxId, uri })
const settingsState = useSettingsState()
const chatThreadsStreamState = useChatThreadsStreamState(threadId)
const isRunning = chatThreadsStreamState?.isRunning
const isDisabled = !!isFeatureNameDisabled('Chat', settingsState) || !applyBoxId
const onAccept = useCallback(() => {
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const onReject = useCallback(() => {
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
if (isDisabled) return null
if (streamState === 'idle-no-changes') {
return null
}
if (streamState === 'idle-has-changes') {
if (isRunning === 'LLM' || isRunning === 'tool') return null
return <>
<IconShell1
Icon={X}
@@ -325,13 +506,13 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string
export const BlockCodeApplyWrapper = ({
children,
initValue,
codeStr,
applyBoxId,
language,
canApply,
uri,
}: {
initValue: string;
codeStr: string;
children: React.ReactNode;
applyBoxId: string;
canApply: boolean;
@@ -340,7 +521,8 @@ export const BlockCodeApplyWrapper = ({
}) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
const currStreamState = currStreamStateRef.current
const name = uri !== 'current' ?
@@ -348,7 +530,7 @@ export const BlockCodeApplyWrapper = ({
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
isSmall={true}
showDot={false}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
onClick={() => { voidOpenFileFn(uri, accessor) }}
/>
: <span>{language}</span>
@@ -364,8 +546,8 @@ export const BlockCodeApplyWrapper = ({
</div>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} language={language} />
</div>
</div>
@@ -9,12 +9,12 @@ import { marked, MarkedToken, Token } from 'marked'
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
import { useAccessor } from '../util/services.js'
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { isAbsolute } from '../../../../../../../base/common/path.js'
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
import { BlockCode } from '../util/inputs.js'
import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js'
import { getBasename, getRelative, voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
export type ChatMessageLocation = {
@@ -89,13 +89,18 @@ const LatexRender = ({ latex }: { latex: string }) => {
// }
}
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
const Codespan = ({ text, className, onClick, tooltip }: { text: string, className?: string, onClick?: () => void, tooltip?: string }) => {
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
return <code
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
onClick={onClick}
{...tooltip ? {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': tooltip,
'data-tooltip-place': 'top',
} : {}}
>
{text}
</code>
@@ -115,8 +120,11 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link: CodespanLocationLink | undefined = undefined
if (rawText.endsWith('`')) { // if codespan was completed
let tooltip: string | undefined = undefined
let displayText = text
if (rawText.endsWith('`')) {
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
@@ -127,41 +135,33 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
setDidComputeCodespanLink(true) // rerender
})
}
if (link?.displayText) {
displayText = link.displayText
}
if (isValidUri(displayText)) {
tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip
displayText = getBasename(displayText)
}
}
const onClick = () => {
if (!link) return;
const selection = link.selection
// open the file
commandService.executeCommand('vscode.open', link.uri).then(() => {
// select the text
setTimeout(() => {
if (!selection) return;
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
editor.setSelection(selection)
editor.revealRange(selection, ScrollType.Immediate)
}, 50) // needed when document was just opened and needs to initialize
})
// Use the updated voidOpenFileFn to open the file and handle selection
if (link.selection)
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
else
voidOpenFileFn(link.uri, accessor);
}
return <Codespan
// text={link?.displayText || text}
text={link?.displayText || text}
text={displayText}
onClick={onClick}
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
tooltip={tooltip || undefined}
/>
}
@@ -319,12 +319,12 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <BlockCodeApplyWrapper
canApply={isCodeblockClosed}
applyBoxId={applyBoxId}
initValue={contents}
codeStr={contents}
language={language}
uri={uri || 'current'}
>
<BlockCode
initValue={contents}
initValue={contents.trimEnd()} // \n\n adds a permanent newline which creates a flash
language={language}
/>
</BlockCodeApplyWrapper>
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { useIsDark, useSidebarState } from '../util/services.js'
import { useIsDark } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { QuickEditChat } from './QuickEditChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
@@ -71,7 +71,7 @@ export const QuickEditChat = ({
startBehavior: 'keep-conflicts',
} as const
await editCodeService.callBeforeStartApplying(opts)
await editCodeService.callBeforeApplyOrEdit(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useIsDark, useSidebarState } from '../util/services.js';
import { useIsDark } from '../util/services.js';
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
@@ -12,8 +12,6 @@ import { SidebarChat } from './SidebarChat.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
const { currentTab: tab } = sidebarState
const isDark = useIsDark()
return <div
@@ -29,34 +27,12 @@ export const Sidebar = ({ className }: { className: string }) => {
`}
>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
const index = tabs.indexOf(tab)
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div> */}
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
<div className={`w-full h-full`}>
<ErrorBoundary>
<SidebarChat />
</ErrorBoundary>
{/* <ErrorBoundary>
<ModelSelectionSettings />
</ErrorBoundary> */}
</div>
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div> */}
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -11,138 +11,6 @@ import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserChe
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
export const OldSidebarThreadSelector = () => {
const accessor = useAccessor()
const sidebarStateService = accessor.get('ISidebarStateService')
return (
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
<div className="w-full relative flex justify-center items-center">
{/* title */}
<h2 className='font-bold text-lg'>{`History`}</h2>
{/* X button at top right */}
<button
type='button'
className='absolute top-0 right-0'
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
>
<IconX
size={16}
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
/>
</button>
</div>
{/* a list of all the past threads */}
{/* <OldPastThreadsList /> */}
</div>
)
}
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
const OldPastThreadsList = () => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return <div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
{sortedThreadIds.length === 0
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false })
}}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
}
const numInitialThreads = 3
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
@@ -313,7 +181,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
// const settingsState = useSettingsState()
// const convertService = accessor.get('IConvertToLLMMessageService')
@@ -353,13 +220,14 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
const detailsHTML = <span
className='gap-1 inline-flex items-center'
// data-tooltip-id='void-tooltip'
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
// data-tooltip-place='top'
>
{/* <span>{numMessages}</span> */}
<span className='opacity-60'>{numMessages}</span>
{` `}
{formatDate(new Date(pastThread.lastModified))}
{/* {` messages `} */}
</span>
return <div
@@ -369,7 +237,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false });
}}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
@@ -377,15 +244,19 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
{/* spinner */}
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
{isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'idle' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
null}
{/* name */}
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
<span className="truncate overflow-hidden text-ellipsis"
data-tooltip-id='void-tooltip'
data-tooltip-content={numMessages + ' messages'}
data-tooltip-place='top'
>{firstMsg}</span>
<span className='opacity-60'>{`(${numMessages})`}</span>
{/* <span className='opacity-60'>{`(${numMessages})`}</span> */}
</span>
<div className="flex items-center gap-x-1 opacity-60">
@@ -9,10 +9,13 @@ type ReturnType<T> = [
// use this if state might be too slow to catch
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
const [_, _setState] = useState(false)
// this actually makes a difference being an int, not a boolean.
// if it's a boolean and changes happen to fast, it goes with old values and leads to *very* weird bugs (like returning JSX, but not actually rendering it)
const [_s, _setState] = useState(0)
const ref = useRef<T>(initVal)
const setState = useCallback((newVal: T) => {
_setState(n => !n) // call rerender
_setState(n => n + 1) // call rerender
ref.current = newVal
}, [])
return [ref, setState]
@@ -17,7 +17,7 @@ import { asCssVariable } from '../../../../../../../platform/theme/common/colorU
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
import { URI } from '../../../../../../../base/common/uri.js';
import { getBasename } from '../sidebar-tsx/SidebarChat.js';
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
@@ -55,12 +55,13 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
type GenerateNextOptions = (optionText: string) => Promise<Option[]>
type Option = {
nameInMenu: string,
fullName: string,
abbreviatedName: string,
iconInMenu: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>, // type for lucide-react components
} & (
| { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined }
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined }
| { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
| { leafNodeType?: undefined, nextOptions: Option[], generateNextOptions?: undefined, }
| { leafNodeType?: undefined, nextOptions?: undefined, generateNextOptions: GenerateNextOptions, }
| { leafNodeType: 'File' | 'Folder', uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
)
@@ -131,7 +132,7 @@ const scoreSubsequence = (text: string, pattern: string): number => {
}
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
const workspaceService = accessor.get('IWorkspaceContextService');
const workspaceFolders = workspaceService.getWorkspace().folders;
@@ -159,7 +160,7 @@ export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor
if (relativePath.startsWith('/')) {
relativePath = relativePath.slice(1);
}
console.log({ folderPath, relativePath, uriPath });
// console.log({ folderPath, relativePath, uriPath });
return relativePath;
}
@@ -173,10 +174,19 @@ export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor
const numOptionsToShow = 100
// TODO make this unique based on other options
const getAbbreviatedName = (relativePath: string) => {
return getBasename(relativePath, 1)
}
const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path: string[], optionText: string): Promise<Option[]> => {
const toolsService = accessor.get('IToolsService')
const searchForFilesOrFolders = async (t: string, searchFor: 'files' | 'folders') => {
try {
@@ -193,8 +203,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
leafNodeType: 'File',
uri: uri,
iconInMenu: File,
nameInMenu: relativePath,
nameToPaste: getBasename(relativePath, 2),
fullName: relativePath,
abbreviatedName: getAbbreviatedName(relativePath),
}
})
return res
@@ -241,7 +251,6 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
for (let i = 0; i < pathParts.length - 1; i++) {
currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`;
console.log('filepath', currentPath);
// Create a proper directory URI
const directoryUri = URI.joinPath(
@@ -258,8 +267,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
leafNodeType: 'Folder',
uri: uri,
iconInMenu: Folder, // Folder
nameInMenu: relativePath,
nameToPaste: getBasename(relativePath, 2)
fullName: relativePath,
abbreviatedName: getAbbreviatedName(relativePath),
})) satisfies Option[];
}
} catch (error) {
@@ -271,13 +280,15 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
const allOptions: Option[] = [
{
nameInMenu: 'files',
fullName: 'files',
abbreviatedName: 'files',
iconInMenu: File,
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [],
},
{
nameInMenu: 'folders',
iconInMenu: FolderClosed,
fullName: 'folders',
abbreviatedName: 'folders',
iconInMenu: Folder,
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [],
},
]
@@ -289,7 +300,7 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
for (const pn of path) {
const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase())
const selectedOption = nextOptionsAtPath.find(o => o.fullName.toLowerCase() === pn.toLowerCase())
if (!selectedOption) return [];
@@ -300,14 +311,20 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
if (generateNextOptionsAtPath) {
nextOptionsAtPath = await generateNextOptionsAtPath(optionText)
}
else if (path.length === 0 && optionText.trim().length > 0) { // (special case): directly search for both files and folders if optionsPath is empty and there's a search term
const filesResults = await searchForFilesOrFolders(optionText, 'files') || [];
const foldersResults = await searchForFilesOrFolders(optionText, 'folders') || [];
nextOptionsAtPath = [...foldersResults, ...filesResults,]
}
const optionsAtPath = nextOptionsAtPath
.filter(o => isSubsequence(o.nameInMenu, optionText))
.filter(o => isSubsequence(o.fullName, optionText))
.sort((a, b) => { // this is a hack but good for now
const scoreA = scoreSubsequence(a.nameInMenu, optionText);
const scoreB = scoreSubsequence(b.nameInMenu, optionText);
const scoreA = scoreSubsequence(a.fullName, optionText);
const scoreB = scoreSubsequence(b.fullName, optionText);
return scoreB - scoreA;
})
.slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints
@@ -354,6 +371,14 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
const [optionIdx, setOptionIdx] = useState<number>(0);
const [options, setOptions] = useState<Option[]>([]);
const [optionText, setOptionText] = useState<string>('');
const [didLoadInitialOptions, setDidLoadInitialOptions] = useState(false);
const currentPathRef = useRef<string>(JSON.stringify([]));
// dont show breadcrums if first page and user hasnt typed anything
const isTypingEnabled = true
const isBreadcrumbsShowing = optionPath.length === 0 && !optionText ? false : true
const insertTextAtCursor = (text: string) => {
const textarea = textAreaRef.current;
if (!textarea) return;
@@ -361,9 +386,21 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// Focus the textarea first
textarea.focus();
// The most reliable way to simulate typing is to use execCommand
// which will trigger all the appropriate native events
document.execCommand('insertText', false, text + ' '); // add space after too
// delete the @ and set the cursor position
// Get cursor position
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
// Get the text before the cursor, excluding the @ symbol that triggered the menu
const textBeforeCursor = textarea.value.substring(0, startPos - 1);
const textAfterCursor = textarea.value.substring(endPos);
// Replace the text including the @ symbol with the selected option
textarea.value = textBeforeCursor + textAfterCursor;
// Set cursor position after the inserted text
const newCursorPos = textBeforeCursor.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// React's onChange relies on a SyntheticEvent system
// The best way to ensure it runs is to call callbacks directly
@@ -379,51 +416,64 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
if (!options.length) { return; }
const option = options[optionIdx];
const newPath = [...optionPath, option.nameInMenu]
const newPath = [...optionPath, option.fullName]
const isLastOption = !option.generateNextOptions && !option.nextOptions
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
setDidLoadInitialOptions(false)
if (isLastOption) {
setIsMenuOpen(false)
insertTextAtCursor(option.nameToPaste)
insertTextAtCursor(option.abbreviatedName)
const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? {
let newSelection: StagingSelectionItem
if (option.leafNodeType === 'File') newSelection = {
type: 'File',
uri: option.uri,
language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '',
state: { wasAddedAsCurrentFile: false }
} : option.leafNodeType === 'Folder' ? {
state: { wasAddedAsCurrentFile: false },
}
else if (option.leafNodeType === 'Folder') newSelection = {
type: 'Folder',
uri: option.uri,
language: undefined,
state: undefined,
} : (undefined as never)
}
else throw new Error(`Unexpected leafNodeType ${option.leafNodeType}`)
chatThreadService.addNewStagingSelection(newSelection)
console.log('selected', option.uri?.fsPath)
}
else {
currentPathRef.current = JSON.stringify(newPath);
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
setOptions(newOpts)
setDidLoadInitialOptions(true)
}
}
const onRemoveOption = async () => {
const newPath = [...optionPath.slice(0, optionPath.length - 1)]
currentPathRef.current = JSON.stringify(newPath);
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
setOptions(newOpts)
}
const onOpenOptionMenu = async () => {
setOptionPath([])
const newPath: [] = []
currentPathRef.current = JSON.stringify([]);
const newOpts = await getOptionsAtPath(accessor, [], '') || []
if (currentPathRef.current !== JSON.stringify([])) { return; }
setOptionPath(newPath)
setOptionText('')
setIsMenuOpen(true);
setOptionIdx(0);
const newOpts = await getOptionsAtPath(accessor, [], '') || []
setOptions(newOpts);
}
const onCloseOptionMenu = () => {
@@ -466,24 +516,36 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
};
}, []);
// debounced
// debounced, but immediate if text is empty
const onPathTextChange = useCallback((newStr: string) => {
setOptionText(newStr);
if (debounceTimerRef.current !== null) {
window.clearTimeout(debounceTimerRef.current);
}
// Set a new timeout to fetch options after a delay
debounceTimerRef.current = window.setTimeout(async () => {
currentPathRef.current = JSON.stringify(optionPath);
const fetchOptions = async () => {
const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || [];
if (currentPathRef.current !== JSON.stringify(optionPath)) { return; }
setOptions(newOpts);
setOptionIdx(0);
debounceTimerRef.current = null;
}, 300);
};
// If text is empty, run immediately without debouncing
if (newStr.trim() === '') {
fetchOptions();
} else {
// Otherwise, set a new timeout to fetch options after a delay
debounceTimerRef.current = window.setTimeout(fetchOptions, 300);
}
}, [optionPath, accessor]);
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isCommandKeyPressed = e.altKey || e.ctrlKey || e.metaKey;
@@ -537,7 +599,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// do nothing
}
else { // letter
onPathTextChange(optionText + e.key)
if (isTypingEnabled) {
onPathTextChange(optionText + e.key)
}
}
}
@@ -715,6 +779,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
return;
}
if (e.key === 'Backspace') { // TODO allow user to undo this.
if (!e.currentTarget.value || (e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0)) { // if there is no text or cursor is at position 0, remove a selection
if (e.metaKey || e.ctrlKey) { // Ctrl+Backspace = remove all
chatThreadService.popStagingSelections(Number.MAX_SAFE_INTEGER)
} else { // Backspace = pop 1 selection
chatThreadService.popStagingSelections(1)
}
return;
}
}
if (e.key === 'Enter') {
// Shift + Enter when multiline = newline
const shouldAddNewline = e.shiftKey && multiline
@@ -740,25 +814,25 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
onWheel={(e) => e.stopPropagation()}
>
{/* Breadcrumbs Header */}
<div className="px-2 py-1 text-void-fg-3 bg-void-bg-2-alt text-sm border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionPath.length || optionText ?
{isBreadcrumbsShowing && <div className="px-2 py-1 text-void-fg-1 bg-void-bg-2-alt border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionText ?
<div className="flex items-center">
{optionPath.map((path, index) => (
{/* {optionPath.map((path, index) => (
<React.Fragment key={index}>
<span>{path}</span>
<ChevronRight size={12} className="mx-1" />
</React.Fragment>
))}
))} */}
<span>{optionText}</span>
</div>
: <div className='opacity-60'>Enter text to filter...</div>
: <div className='opacity-50'>Enter text to filter...</div>
}
</div>
</div>}
{/* Options list */}
<div className='max-h-[400px] w-full max-w-full overflow-y-auto overflow-x-auto'>
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap text-sm opacity-70">
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap">
{options.length === 0 ?
<div className="text-void-fg-3 px-3 py-0.5">No results found</div>
: options.map((o, oIdx) => {
@@ -767,20 +841,25 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// Option
<div
ref={oIdx === optionIdx ? selectedOptionRef : null}
key={o.nameInMenu}
key={o.fullName}
className={`
flex items-center gap-2
px-3 py-0.5 cursor-pointer bg-void-bg-2-alt
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
px-3 py-1 cursor-pointer
${oIdx === optionIdx ? 'bg-blue-500 text-white/80' : 'bg-void-bg-2-alt text-void-fg-1'}
`}
onClick={() => { onSelectOption(); }}
onMouseOver={() => { setOptionIdx(oIdx) }}
onMouseMove={() => { setOptionIdx(oIdx) }}
>
{<o.iconInMenu size={12} />}
<span className="text-void-fg-1">{o.nameInMenu}</span>
<span>{o.abbreviatedName}</span>
{o.fullName && o.fullName !== o.abbreviatedName && <span className="opacity-60 text-sm">{o.fullName}</span>}
{o.nextOptions || o.generateNextOptions ? (
<ChevronRight size={12} />
) : null}
</div>
)
})
@@ -803,15 +882,44 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
compact?: boolean;
passwordBlur?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) => {
// Create a ref for the input element to maintain the same DOM node between renders
const inputRef = useRef<HTMLInputElement>(null);
// Track if we need to restore selection
const selectionRef = useRef<{ start: number | null, end: number | null }>({
start: null,
end: null
});
// Handle value changes without recreating the input
useEffect(() => {
const input = inputRef.current;
if (input && input.value !== value) {
// Store current selection positions
selectionRef.current.start = input.selectionStart;
selectionRef.current.end = input.selectionEnd;
// Update the value
input.value = value;
// Restore selection if we had it before
if (selectionRef.current.start !== null && selectionRef.current.end !== null) {
input.setSelectionRange(selectionRef.current.start, selectionRef.current.end);
}
}
}, [value]);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChangeValue(e.target.value);
}, [onChangeValue]);
return (
<input
value={value}
onChange={(e) => onChangeValue(e.target.value)}
ref={inputRef}
defaultValue={value} // Use defaultValue instead of value to avoid recreation
onChange={handleChange}
placeholder={placeholder}
disabled={disabled}
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
rounded
@@ -1329,7 +1437,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
key={optionName}
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
transition-all duration-100
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
${thisOptionIsSelected ? 'bg-blue-500 text-white/80' : 'hover:bg-blue-500 hover:text-white/80'}
`}
onClick={() => {
onChangeOption(option);
@@ -1351,7 +1459,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
</div>
<span className="flex justify-between items-center w-full gap-x-1">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
<span className='opacity-60'>{optionDetail}</span>
</span>
</div>
);
@@ -6,7 +6,6 @@
import React, { useState, useEffect, useCallback } from 'react'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
@@ -22,8 +21,9 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IVoidCertificateService } from '../../../../common/voidCertificateService.js';
import { IExtensionTransferService } from '../../../../../../../workbench/contrib/void/browser/extensionTransferService.js'
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
@@ -49,8 +49,9 @@ import { INativeHostService } from '../../../../../../../platform/native/common/
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { IToolsService } from '../../../toolsService.js'
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
import { ISearchService } from '../../../../../../services/search/common/search.js'
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
import { ISearchService } from '../../../../../../services/search/common/search.js'
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@@ -58,9 +59,6 @@ import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
let chatThreadsState: ThreadsState
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
@@ -91,7 +89,6 @@ export const _registerServices = (accessor: ServicesAccessor) => {
_registerAccessor(accessor)
const stateServices = {
sidebarStateService: accessor.get(ISidebarStateService),
chatThreadsStateService: accessor.get(IChatThreadService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
@@ -101,15 +98,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
modelService: accessor.get(IModelService),
}
const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
sidebarState = sidebarStateService.state
disposables.push(
sidebarStateService.onDidChangeState(() => {
sidebarState = sidebarStateService.state
sidebarStateListeners.forEach(l => l(sidebarState))
})
)
chatThreadsState = chatThreadsStateService.state
disposables.push(
@@ -147,8 +139,8 @@ export const _registerServices = (accessor: ServicesAccessor) => {
colorThemeState = themeService.getColorTheme().type
disposables.push(
themeService.onDidColorThemeChange(({ theme }) => {
colorThemeState = theme.type
themeService.onDidColorThemeChange(({ type }) => {
colorThemeState = type
colorThemeStateListeners.forEach(l => l(colorThemeState))
})
)
@@ -193,8 +185,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IEditCodeService: accessor.get(IEditCodeService),
ISidebarStateService: accessor.get(ISidebarStateService),
IChatThreadService: accessor.get(IChatThreadService),
IVoidCertificateService: accessor.get(IVoidCertificateService),
IInstantiationService: accessor.get(IInstantiationService),
ICodeEditorService: accessor.get(ICodeEditorService),
@@ -223,6 +215,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IToolsService: accessor.get(IToolsService),
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
ITerminalService: accessor.get(ITerminalService),
IExtensionManagementService: accessor.get(IExtensionManagementService),
IExtensionTransferService: accessor.get(IExtensionTransferService),
} as const
return reactAccessor
@@ -250,16 +244,6 @@ export const useAccessor = () => {
// -- state of services --
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {
ss(sidebarState)
sidebarStateListeners.add(ss)
return () => { sidebarStateListeners.delete(ss) }
}, [ss])
return s
}
export const useSettingsState = () => {
const [s, ss] = useState(settingsState)
useEffect(() => {
@@ -9,9 +9,19 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
import '../styles.css'
import { useCallback, useEffect, useState, useRef } from 'react';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
import { Check, EllipsisVertical, Menu, MoveDown, MoveLeft, MoveRight, MoveUp, X } from 'lucide-react';
import {
VOID_GOTO_NEXT_DIFF_ACTION_ID,
VOID_GOTO_PREV_DIFF_ACTION_ID,
VOID_GOTO_NEXT_URI_ACTION_ID,
VOID_GOTO_PREV_URI_ACTION_ID,
VOID_ACCEPT_FILE_ACTION_ID,
VOID_REJECT_FILE_ACTION_ID,
VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
VOID_REJECT_ALL_DIFFS_ACTION_ID
} from '../../../actionIDs.js';
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const isDark = useIsDark()
@@ -25,14 +35,55 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => {
if (len === 0) return null
return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset
}
export const AcceptAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={`
px-2 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
h-full rounded-none
cursor-pointer
${className}
`}
style={{
backgroundColor: 'var(--vscode-button-background)',
color: 'var(--vscode-button-foreground)',
border: 'none',
}}
type='button'
onClick={onClick}
{...props}
>
{text ? <span>{text}</span> : <Check size={16} />}
</button>
)
export const RejectAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={`
px-2 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
h-full rounded-none
cursor-pointer
${className}
`}
style={{
backgroundColor: 'var(--vscode-button-secondaryBackground)',
color: 'var(--vscode-button-secondaryForeground)',
border: 'none',
}}
type='button'
onClick={onClick}
{...props}
>
{text ? <span>{text}</span> : <X size={16} />}
</button>
)
const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const editorService = accessor.get('ICodeEditorService')
@@ -40,12 +91,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const commandService = accessor.get('ICommandService')
const commandBarService = accessor.get('IVoidCommandBarService')
const voidModelService = accessor.get('IVoidModelService')
const keybindingService = accessor.get('IKeybindingService')
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
// useEffect(() => {
// console.log('MOUNTING!!!')
// }, [])
const [showAcceptRejectAllButtons, setShowAcceptRejectAllButtons] = useState(false)
// latestUriIdx is used to remember place in leftRight
const _latestValidUriIdxRef = useRef<number | null>(null)
@@ -70,56 +118,18 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const s = commandBarService.stateOfURI[uri.fsPath]
if (!s) return
const { diffIdx } = s
goToDiffIdx(diffIdx ?? 0)
commandBarService.goToDiffIdx(diffIdx ?? 0)
}, 50)
}, [uri, commandBarService])
if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files
const getNextDiffIdx = (step: 1 | -1) => {
// check undefined
if (!uri) return null
const s = commandBarState[uri.fsPath]
if (!s) return null
const { diffIdx, sortedDiffIds } = s
// get next idx
const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step)
return nextDiffIdx
}
const goToDiffIdx = (idx: number | null) => {
if (idx === null) return
// check undefined
if (!uri) return
const s = commandBarState[uri.fsPath]
if (!s) return
const { sortedDiffIds } = s
// reveal
const diffid = sortedDiffIds[idx]
if (diffid === undefined) return
const diff = editCodeService.diffOfId[diffid]
if (!diff) return
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
commandBarService.setDiffIdx(uri, idx)
}
const getNextUriIdx = (step: 1 | -1) => {
return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step)
}
const goToURIIdx = async (idx: number | null) => {
if (idx === null) return
const nextURI = sortedCommandBarURIs[idx]
editCodeService.diffAreasOfURI
const { model } = await voidModelService.getModelSafe(nextURI)
if (model) {
// switch to the URI
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
}
}
// Using service methods directly
const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null
const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : []
const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : []
const isADiffInThisFile = sortedDiffIds.length !== 0
const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0
const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0
@@ -127,191 +137,247 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const streamState = uri ? commandBarService.getStreamState(uri) : null
const showAcceptRejectAll = streamState === 'idle-has-changes'
const nextDiffIdx = getNextDiffIdx(1)
const prevDiffIdx = getNextDiffIdx(-1)
const nextURIIdx = getNextUriIdx(1)
const prevURIIdx = getNextUriIdx(-1)
const nextDiffIdx = commandBarService.getNextDiffIdx(1)
const prevDiffIdx = commandBarService.getNextDiffIdx(-1)
const nextURIIdx = commandBarService.getNextUriIdx(1)
const prevURIIdx = commandBarService.getNextUriIdx(-1)
const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile)
const upButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}// --border border-void-border-3 focus:border-void-border-1
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(prevDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(prevDiffIdx);
}
}}
></button>
const downButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(nextDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(nextDiffIdx);
}
}}
></button>
const leftButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(prevURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(prevURIIdx);
}
}}
></button>
const rightButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(nextURIIdx);
}
}}
></button>
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null
// accept/reject if current URI has changes
const onAcceptAll = () => {
const onAcceptFile = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Accept All', {})
metricsService.capture('Accept File', {})
}
const onRejectAll = () => {
const onRejectFile = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Reject All', {})
metricsService.capture('Reject File', {})
}
const onAcceptAll = () => {
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
metricsService.capture('Accept All', {})
setShowAcceptRejectAllButtons(false);
}
const onRejectAll = () => {
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
metricsService.capture('Reject All', {})
setShowAcceptRejectAllButtons(false);
}
const _upKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_DIFF_ACTION_ID);
const _downKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_DIFF_ACTION_ID);
const _leftKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_URI_ACTION_ID);
const _rightKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_URI_ACTION_ID);
const _acceptFileKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_FILE_ACTION_ID);
const _rejectFileKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_FILE_ACTION_ID);
const _acceptAllKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_ALL_DIFFS_ACTION_ID);
const _rejectAllKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_ALL_DIFFS_ACTION_ID);
const upKeybindLabel = editCodeService.processRawKeybindingText(_upKeybinding?.getLabel() || '');
const downKeybindLabel = editCodeService.processRawKeybindingText(_downKeybinding?.getLabel() || '');
const leftKeybindLabel = editCodeService.processRawKeybindingText(_leftKeybinding?.getLabel() || '');
const rightKeybindLabel = editCodeService.processRawKeybindingText(_rightKeybinding?.getLabel() || '');
const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getAriaLabel() || '');
const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getAriaLabel() || '');
const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getAriaLabel() || '');
const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getAriaLabel() || '');
if (!isADiffZoneInAnyFile) return null
// const acceptAllButton = <button
// className='text-nowrap'
// onClick={onAcceptAll}
// style={{
// backgroundColor: acceptAllBg,
// border: acceptBorder,
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Accept File
// </button>
// const rejectAllButton = <button
// className='text-nowrap'
// onClick={onRejectAll}
// style={{
// backgroundColor: rejectBg,
// border: rejectBorder,
// color: 'white',
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Reject File
// </button>
const acceptAllButton = <AcceptAllButtonWrapper
text={'Keep Changes'}
onClick={onAcceptAll}
/>
const rejectAllButton = <RejectAllButtonWrapper
text={'Reject All'}
onClick={onRejectAll}
/>
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
{acceptAllButton}
{rejectAllButton}
</div>
// const closeCommandBar = useCallback(() => {
// commandService.executeCommand('void.hideCommandBar');
// }, [commandService]);
// const hideButton = <button
// className='ml-auto pointer-events-auto'
// onClick={closeCommandBar}
// style={{
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// title="Close command bar"
// >x
// </button>
const leftRightUpDownButtons = <div className='p-1 gap-1 flex flex-col items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 w-full'>
<div className="flex flex-col gap-1">
{/* Changes in file */}
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
{upButton}
{downButton}
<span className="min-w-16 px-2 text-xs leading-[1]">
{isADiffInThisFile ?
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming' ?
'No changes yet'
: `No changes`
}
</span>
// For pages without a current file index, show a simplified command bar
if (currFileIdx === null) {
return (
<div className="pointer-events-auto">
<div className="flex bg-void-bg-2 shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
<div className="flex items-center px-3">
<span className="text-xs whitespace-nowrap">
{`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`}
</span>
</div>
<button
className="text-xs whitespace-nowrap cursor-pointer flex items-center justify-center gap-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] hover:opacity-90 h-full px-3"
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(nextURIIdx);
}
}}
>
Next <MoveRight className='size-3 my-1' />
</button>
</div>
</div>
);
}
{/* Files */}
<div className={`${!isADiffZoneInAnyFile ? 'hidden' : ''} flex items-center ${leftRightDisabled ? 'opacity-50' : ''}`}>
{leftButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
{rightButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
<span className="min-w-16 px-2 text-xs leading-[1]">
{currFileIdx !== null ?
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
}
</span>
return (
<div className="pointer-events-auto">
{/* Accept All / Reject All buttons that appear when the vertical ellipsis is clicked */}
{showAcceptRejectAllButtons && showAcceptRejectAll && (
<div className="flex justify-end mb-1">
<div className="inline-flex bg-void-bg-2 rounded shadow-md border border-void-border-2 overflow-hidden">
<div className="flex items-center [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
<AcceptAllButtonWrapper
// text={`Accept All${acceptAllKeybindLabel ? ` ${acceptAllKeybindLabel}` : ''}`}
text={`Accept All`}
data-tooltip-id='void-tooltip'
data-tooltip-content={acceptAllKeybindLabel}
data-tooltip-delay-show={500}
onClick={onAcceptAll}
/>
<RejectAllButtonWrapper
// text={`Reject All${rejectAllKeybindLabel ? ` ${rejectAllKeybindLabel}` : ''}`}
text={`Reject All`}
data-tooltip-id='void-tooltip'
data-tooltip-content={rejectAllKeybindLabel}
data-tooltip-delay-show={500}
onClick={onRejectAll}
/>
</div>
</div>
</div>
)}
<div className="flex items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:px-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
{/* Diff Navigation Group */}
<div className="flex items-center py-0.5">
<button
className="cursor-pointer"
disabled={upDownDisabled}
onClick={() => commandBarService.goToDiffIdx(prevDiffIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToDiffIdx(prevDiffIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${upKeybindLabel ? `${upKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveUp className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
<span className={`text-xs whitespace-nowrap px-1 ${!isADiffInThisFile ? 'opacity-70' : ''}`}>
{isADiffInThisFile
? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming'
? 'No changes yet'
: 'No changes'
}
</span>
<button
className="cursor-pointer"
disabled={upDownDisabled}
onClick={() => commandBarService.goToDiffIdx(nextDiffIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToDiffIdx(nextDiffIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${downKeybindLabel ? `${downKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveDown className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
</div>
{/* File Navigation Group */}
<div className="flex items-center py-0.5">
<button
className="cursor-pointer"
disabled={leftRightDisabled}
onClick={() => commandBarService.goToURIIdx(prevURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(prevURIIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveLeft className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
<span className="text-xs whitespace-nowrap px-1 mx-0.5">
{currFileIdx !== null
? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}`
}
</span>
<button
className="cursor-pointer"
disabled={leftRightDisabled}
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(nextURIIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveRight className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
</div>
{/* Accept/Reject buttons - only shown when appropriate */}
{showAcceptRejectAll && (
<div className='flex self-stretch gap-0 !px-0 !py-0'>
<AcceptAllButtonWrapper
// text={`Accept File${acceptFileKeybindLabel ? ` ${acceptFileKeybindLabel}` : ''}`}
text={`Accept File`}
data-tooltip-id='void-tooltip'
data-tooltip-content={acceptFileKeybindLabel}
data-tooltip-delay-show={500}
onClick={onAcceptFile}
/>
<RejectAllButtonWrapper
// text={`Reject File${rejectFileKeybindLabel ? ` ${rejectFileKeybindLabel}` : ''}`}
text={`Reject File`}
data-tooltip-id='void-tooltip'
data-tooltip-content={rejectFileKeybindLabel}
data-tooltip-delay-show={500}
onClick={onRejectFile}
/>
</div>
)}
{/* Triple colon menu button */}
{showAcceptRejectAll && <div className='!px-0 !py-0 self-stretch flex justify-center items-center'>
<div
className="cursor-pointer px-1 self-stretch flex justify-center items-center"
onClick={() => setShowAcceptRejectAllButtons(!showAcceptRejectAllButtons)}
>
<EllipsisVertical
className="size-3"
/>
</div>
</div>}
</div>
</div>
</div>
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
{showAcceptRejectAll && acceptRejectAllButtons}
{leftRightUpDownButtons}
</div>
)
}
@@ -6,10 +6,9 @@
import { useEffect, useRef, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
import { isLinux } from '../../../../../../../base/common/platform.js';
@@ -27,9 +26,10 @@ export const VoidOnboarding = () => {
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div
className={`
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full z-[99999]
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ErrorBoundary>
<VoidOnboardingContent />
@@ -90,6 +90,187 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch
}
// Onboarding
// =============================================
// New AddProvidersPage Component and helpers
// =============================================
const tabNames = ['Free', 'Paid', 'Local'] as const;
type TabName = typeof tabNames[number] | 'Cloud/Other';
// Data for cloud providers tab
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'openAICompatible'];
// Data structures for provider tabs
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
Free: ['gemini', 'openRouter'],
Local: localProviderNames,
Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[],
'Cloud/Other': cloudProviders,
};
const descriptionOfTab: Record<TabName, string> = {
Free: `Providers with a 100% free tier. Add as many as you'd like!`,
Paid: `Connect directly with any provider (bring your own key).`,
Local: `Active providers should appear automatically. Add as many as you'd like! `,
'Cloud/Other': `Add as many as you'd like! Reach out for custom configuration requests.`,
};
const featureNameMap: { display: string, featureName: FeatureName }[] = [
{ display: 'Chat', featureName: 'Chat' },
{ display: 'Quick Edit', featureName: 'Ctrl+K' },
{ display: 'Autocomplete', featureName: 'Autocomplete' },
{ display: 'Fast Apply', featureName: 'Apply' },
];
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {
const [currentTab, setCurrentTab] = useState<TabName>('Free');
const settingsState = useSettingsState();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Clear error message after 5 seconds
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null;
if (errorMessage) {
timeoutId = setTimeout(() => {
setErrorMessage(null);
}, 5000);
}
// Cleanup function to clear the timeout if component unmounts or error changes
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [errorMessage]);
return (<div className="flex flex-col md:flex-row w-full h-[80vh] gap-6 max-w-[900px] mx-auto relative">
{/* Left Column */}
<div className="md:w-1/4 w-full flex flex-col gap-6 p-6 border-none border-void-border-2 h-full overflow-y-auto">
{/* Tab Selector */}
<div className="flex md:flex-col gap-2">
{[...tabNames, 'Cloud/Other'].map(tab => (
<button
key={tab}
className={`py-2 px-4 rounded-md text-left ${currentTab === tab
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'
} transition-all duration-200`}
onClick={() => {
setCurrentTab(tab as TabName);
setErrorMessage(null); // Reset error message when changing tabs
}}
>
{tab}
</button>
))}
</div>
{/* Feature Checklist */}
<div className="flex flex-col gap-1 mt-4 text-sm opacity-80">
{featureNameMap.map(({ display, featureName }) => {
const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null;
return (
<div key={featureName} className="flex items-center gap-2">
{hasModel ? (
<Check className="w-4 h-4 text-emerald-500" />
) : (
<div className="w-3 h-3 rounded-full flex items-center justify-center">
<div className="w-1 h-1 rounded-full bg-white/70"></div>
</div>
)}
<span>{display}</span>
</div>
);
})}
</div>
</div>
{/* Right Column */}
<div className="flex-1 flex flex-col items-center justify-start p-6 h-full overflow-y-auto">
<div className="text-5xl mb-2 text-center w-full">Add a Provider</div>
<div className="w-full max-w-xl mt-4 mb-10">
<div className="text-4xl font-light my-4 w-full">{currentTab}</div>
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">{descriptionOfTab[currentTab]}</div>
</div>
{providerNamesOfTab[currentTab].map((providerName) => (
<div key={providerName} className="w-full max-w-xl mb-10">
<div className="text-xl mb-2">
Add {displayInfoOfProviderName(providerName).title}
{providerName === 'gemini' && (
<span
data-tooltip-id="void-tooltip-provider-info"
data-tooltip-content="Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500. We recommend using models down the line as you run out of free credits."
data-tooltip-place="right"
className="ml-1 text-xs align-top text-blue-400"
>*</span>
)}
{providerName === 'openRouter' && (
<span
data-tooltip-id="void-tooltip-provider-info"
data-tooltip-content="OpenRouter offers 50 free messages a day, and 1000 if you deposit $10. Only applies to models labeled ':free'."
data-tooltip-place="right"
className="ml-1 text-xs align-top text-blue-400"
>*</span>
)}
</div>
<div>
<SettingsForProvider providerName={providerName} showProviderTitle={false} showProviderSuggestions={true} />
</div>
{providerName === 'ollama' && <OllamaSetupInstructions />}
</div>
))}
{(currentTab === 'Local' || currentTab === 'Cloud/Other') && (
<div className="w-full max-w-xl mt-8 bg-void-bg-2/50 rounded-lg p-6 border border-void-border-4">
<div className="flex items-center gap-2 mb-4">
<div className="text-xl font-medium">Models</div>
</div>
{currentTab === 'Local' && (
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">Local models should be detected automatically. You can add custom models below.</div>
)}
{currentTab === 'Local' && <ModelDump filteredProviders={localProviderNames} />}
{currentTab === 'Cloud/Other' && <ModelDump filteredProviders={cloudProviders} />}
</div>
)}
{/* Navigation buttons in right column */}
<div className="flex flex-col items-end w-full mt-auto pt-8">
{errorMessage && (
<div className="text-amber-400 mb-2 text-sm opacity-80 transition-opacity duration-300">{errorMessage}</div>
)}
<div className="flex items-center gap-2">
<PreviousButton onClick={() => setPageIndex(pageIndex - 1)} />
<NextButton
onClick={() => {
const isDisabled = isFeatureNameDisabled('Chat', settingsState)
if (!isDisabled) {
setPageIndex(pageIndex + 1);
setErrorMessage(null);
} else {
// Show error message
setErrorMessage("Please set up at least one Chat model before moving on.");
}
}}
/>
</div>
</div>
</div>
</div>);
};
// =============================================
// OnboardingPage
// title:
// div
@@ -179,7 +360,7 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
className?: string,
}) => {
return (
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
<div className={`h-[80vh] text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
{bottom && <div className='w-full pb-8'>{bottom}</div>}
@@ -188,8 +369,6 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
}
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
// for now just link to the ollama download page
return <a
href={`https://ollama.com/library/${modelName}`}
@@ -200,76 +379,6 @@ const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb
<ExternalLink className="w-3.5 h-3.5" />
</a>
// if (isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Uninstall</span>
// <IconShell1
// className="ml-1"
// Icon={Trash}
// onClick={() => {
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (isModelInstalling) {
// return <div className="flex items-center">
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
// <IconShell1
// className="ml-1"
// Icon={Square}
// onClick={() => {
// // abort()
// // TODO!!!!!!!!!!! don't do this
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (!isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
// <IconShell1
// className="ml-1"
// Icon={Download}
// onClick={() => {
// // this is a check for whether the model was installed:
// if (isModelInstalling) return
// // TODO!!!!!! don't do this
// // install(modelname), callback = setIsModelInstalling(false);
// setIsModelInstalling(true);
// }}
// />
// </div>
// }
// return <></>
}
@@ -306,114 +415,7 @@ const abbreviateNumber = (num: number): string => {
}
}
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
// const providerCapabilities = getProviderCapabilities(providerName)
// info used to show the table
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.type !== 'custom',
isDownloaded: true
}
})
// special case columns for ollama; show recommended models as default
if (providerName === 'ollama') {
for (const modelName of ollamaRecommendedModels) {
if (modelName in infoOfModelName) continue
infoOfModelName[modelName] = {
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
showAsDefault: true,
}
}
}
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
<thead>
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
</tr>
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
const capabilities = getModelCapabilities(providerName, modelName)
const {
downloadable,
cost,
supportsFIM,
reasoningCapabilities,
contextWindow,
isUnrecognizedModel,
maxOutputTokens,
supportsSystemMessage,
} = capabilities
// TODO update this when tools work
const removeModelButton = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
>
<X className="w-3.5 h-3.5" />
</button>
return (
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
</td>
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
)
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<ErrorBoundary>
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true}
/>
</ErrorBoundary>
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
@@ -431,22 +433,16 @@ const PrimaryActionButton = ({ children, className, ringSize, ...props }: { chil
${ringSize === 'xl' ? `
gap-2 px-16 py-8
hover:ring-8 active:ring-8
transition-all duration-300 ease-in-out
`
: ringSize === 'screen' ? `
gap-2 px-16 py-8
ring-[3000px]
transition-all duration-1000 ease-in-out
`: ringSize === undefined ? `
gap-1 px-4 py-2
hover:ring-2 active:ring-2
transition-all duration-300 ease-in-out
`: ''}
hover:ring-black/90 dark:hover:ring-white/90
active:ring-black/90 dark:active:ring-white/90
rounded-lg
group
${className}
@@ -534,7 +530,6 @@ const VoidOnboardingContent = () => {
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
</div>
@@ -612,7 +607,7 @@ const VoidOnboardingContent = () => {
delayMs={1000}
>
<PrimaryActionButton
onClick={() => { setPageIndex(pageIndex + 1) }}
onClick={() => { setPageIndex(1) }}
>
Get Started
</PrimaryActionButton>
@@ -621,255 +616,13 @@ const VoidOnboardingContent = () => {
</div>
}
/>,
1: <OnboardingPageShell
hasMaxWidth={false}
top={<></>}
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
<button
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<DollarSign size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
</button>
<button
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Lock size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Private</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
</button>
<button
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Brain size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
</button>
</div>
</div>}
bottom={
<div className='mx-auto w-full max-w-[800px]'>
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
</div>
1: <OnboardingPageShell hasMaxWidth={false}
content={
<AddProvidersPage pageIndex={pageIndex} setPageIndex={setPageIndex} />
}
/>,
2: <OnboardingPageShell
top={
<>
{/* Title */}
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
{/* Preference Selector */}
<div
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
>
{[
{ id: 'smart', label: 'Intelligent' },
{ id: 'private', label: 'Private' },
{ id: 'cheap', label: 'Affordable' },
{ id: 'all', label: 'All' }
].map(option => (
<ErrorBoundary
key={option.id}
>
<button
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
</ErrorBoundary>
))}
</div>
{/* Provider Buttons - Modified to use separate components for each tab */}
<div className="mb-2 w-full">
{/* Intelligent tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Private tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Affordable tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* All tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
</div>
{/* Description */}
<ErrorBoundary>
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</ErrorBoundary>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
{/* Models Table */}
<ErrorBoundary>
<TableOfModelsForProvider providerName={selectedProviderName} />
</ErrorBoundary>
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8 mx-auto'>
<ErrorBoundary>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<div className='my-4'>
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
</div>
</div>
</ErrorBoundary>
<ErrorBoundary>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
</ErrorBoundary>
{/* Button and status indicators */}
<ErrorBoundary>
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
</ErrorBoundary>
</div>
</div>}
</>
}
bottom={
<ErrorBoundary>
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
</ErrorBoundary>
}
/>,
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
// <FadeIn>
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
// </div>
// </FadeIn>
// {prevAndNextButtons}
// </div>,
3: <OnboardingPageShell
content={
<div>
@@ -884,32 +637,11 @@ const VoidOnboardingContent = () => {
</div>
}
bottom={lastPagePrevAndNextButtons}
// bottom={prevAndNextButtons}
/>,
// 4: <OnboardingPageShell
// content={
// <>
// <div
// className='flex justify-center'
// >
// <PrimaryActionButton
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
// className='text-4xl'
// >Enter the Void</PrimaryActionButton>
// </div>
// </>
// }
// bottom={
// <PreviousButton
// onClick={() => { setPageIndex(pageIndex - 1) }}
// />
// }
// />,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
return <div key={pageIndex} className="w-full h-[80vh] text-left mx-auto flex flex-col items-center justify-center">
<ErrorBoundary>
{contentOfIdx[pageIndex]}
</ErrorBoundary>
@@ -56,7 +56,7 @@ const MemoizedModelDropdown = ({ featureName, className }: { featureName: Featur
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode }))
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode, overridesOfModel: settingsState.overridesOfModel }))
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
File diff suppressed because it is too large Load Diff
@@ -45,7 +45,7 @@ export const VoidTooltip = () => {
<>
<style>
{`
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings {
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings, #void-tooltip-provider-info {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
@@ -67,7 +67,7 @@ export const VoidTooltip = () => {
color: white;
}
#void-tooltip-ollama-settings {
#void-tooltip-ollama-settings, #void-tooltip-provider-info {
background-color: var(--vscode-editor-background);
color: var(--vscode-input-foreground);
}
@@ -112,14 +112,25 @@ export const VoidTooltip = () => {
</div>
<div style={{ marginBottom: 4 }}>
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>gemma3</span>
</div>
<div>
<div style={{ marginBottom: 4 }}>
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder</span>
</div>
<div style={{ marginBottom: 0 }}>
<span style={{ opacity: 0.8 }}>Use the largest version of these you can!</span>
</div>
</div>
</Tooltip>
<Tooltip
id="void-tooltip-provider-info"
border='1px solid rgba(100,100,100,.2)'
opacity={1}
delayShow={50}
style={{ pointerEvents: 'all', userSelect: 'text', fontSize: 11, maxWidth: '280px', paddingTop:'8px', paddingBottom:'8px' }}
/>
</>
);
};
@@ -14,23 +14,18 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { VOID_VIEW_ID } from './sidebarPane.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.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';
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { localize2 } from '../../../../nls.js';
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
import { IChatThreadService } from './chatThreadService.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
// ---------- Register commands and keybindings ----------
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
if (!range)
return null
@@ -72,68 +67,21 @@ registerAction2(class extends Action2 {
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
const viewsService = accessor.get(IViewsService)
const chatThreadsService = accessor.get(IChatThreadService)
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID)
await chatThreadsService.focusCurrentChat()
}
})
// Action: when press ctrl+L, show the sidebar chat and add to the selection
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
registerAction2(class extends Action2 {
constructor() {
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
if (!model)
return
const metricsService = accessor.get(IMetricsService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('Ctrl+L', {})
const editor = editorService.getActiveCodeEditor()
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// select whole lines
if (selectionRange) {
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
}
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
state: { wasAddedAsCurrentFile: false }
} : {
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false }
}
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.addNewStagingSelection(newSelection)
}
});
// cmd L
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_CTRL_L_ACTION_ID,
f1: true,
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
title: localize2('voidCmdL', 'Void: Add Selection to Chat'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension
@@ -141,72 +89,119 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
// Get services
const commandService = accessor.get(ICommandService)
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
}
})
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
// New chat menu button
registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.newChatAction',
title: 'New Chat',
icon: { id: 'add' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const viewsService = accessor.get(IViewsService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Chat Navigation', { type: 'New Chat' })
const editorService = accessor.get(ICodeEditorService)
const chatThreadService = accessor.get(IChatThreadService)
openNewThreadAndFireFocus(accessor)
metricsService.capture('Ctrl+L', {})
// capture selection and model before opening the chat panel
const editor = editorService.getActiveCodeEditor()
const model = editor?.getModel()
if (!model) return
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// open panel
const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
if (!wasAlreadyOpen) {
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
// Add selection to chat
// add line selection
if (selectionRange) {
editor?.setSelection({
startLineNumber: selectionRange.startLineNumber,
endLineNumber: selectionRange.endLineNumber,
startColumn: 1,
endColumn: Number.MAX_SAFE_INTEGER
})
chatThreadService.addNewStagingSelection({
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false },
})
}
// add file
else {
chatThreadService.addNewStagingSelection({
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
state: { wasAddedAsCurrentFile: false },
})
}
await chatThreadService.focusCurrentChat()
}
})
// New chat keybind
// New chat keybind + menu button
const VOID_CMD_SHIFT_L_ACTION_ID = 'void.cmdShiftL'
registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.newChatKeybindAction',
title: 'New Chat Keybind',
id: VOID_CMD_SHIFT_L_ACTION_ID,
title: 'New Chat',
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension,
},
icon: { id: 'add' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
const chatThreadsService = accessor.get(IChatThreadService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('Chat Navigation', { type: 'Start New Chat' })
openNewThreadAndFireFocus(accessor)
// get current selections and value to transfer
const oldThreadId = chatThreadsService.state.currentThreadId
const oldThread = chatThreadsService.state.allThreads[oldThreadId]
// add user's selection to chat
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
const oldSelns = oldThread?.state.stagingSelections
const oldVal = oldUI?.textAreaRef.current?.value
// open and focus new thread
chatThreadsService.openNewThread()
await chatThreadsService.focusCurrentChat()
// set new thread values
const newThreadId = chatThreadsService.state.currentThreadId
const newThread = chatThreadsService.state.allThreads[newThreadId]
const newUI = await newThread?.state.mountedInfo?.whenMounted
chatThreadsService.setCurrentThreadState({ stagingSelections: oldSelns, })
if (newUI?.textAreaRef?.current && oldVal) newUI.textAreaRef.current.value = oldVal
// if has selection, add it
const editor = editorService.getActiveCodeEditor()
const model = editor?.getModel()
if (!model) return
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
if (!selectionRange) return
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
chatThreadsService.addNewStagingSelection({
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false },
})
}
})
@@ -229,18 +224,12 @@ registerAction2(class extends Action2 {
return;
}
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'History' })
openNewThreadAndFireFocus(accessor)
// doesnt do anything right now
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
stateService.fireBlurChat()
commandService.executeCommand(VOID_CMD_SHIFT_L_ACTION_ID)
}
})
@@ -265,28 +254,28 @@ registerAction2(class extends Action2 {
export class TabSwitchListener extends Disposable {
// export class TabSwitchListener extends Disposable {
constructor(
onSwitchTab: () => void,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
// constructor(
// onSwitchTab: () => void,
// @ICodeEditorService private readonly _editorService: ICodeEditorService,
// ) {
// super()
// when editor switches tabs (models)
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(editor.onDidChangeModel(e => {
if (e.newModelUrl?.scheme !== 'file') return
onSwitchTab()
}))
}
// // when editor switches tabs (models)
// const addTabSwitchListeners = (editor: ICodeEditor) => {
// this._register(editor.onDidChangeModel(e => {
// if (e.newModelUrl?.scheme !== 'file') return
// onSwitchTab()
// }))
// }
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
}
// const initializeEditor = (editor: ICodeEditor) => {
// addTabSwitchListeners(editor)
// }
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
}
}
// // initialize current editors + any new editors
// for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
// this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// }
// }
@@ -1,82 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
// service that manages sidebar's state
export type VoidSidebarState = {
isHistoryOpen: boolean; // this isn't doing anything right now
currentTab: 'chat';
}
export interface ISidebarStateService {
readonly _serviceBrand: undefined;
readonly state: VoidSidebarState; // readonly to the user
setState(newState: Partial<VoidSidebarState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
}
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
class VoidSidebarStateService extends Disposable implements ISidebarStateService {
_serviceBrand: undefined;
static readonly ID = 'voidSidebarStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
private readonly _onFocusChat = new Emitter<void>();
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
private readonly _onBlurChat = new Emitter<void>();
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
// state
state: VoidSidebarState
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
super()
// initial state
this.state = { isHistoryOpen: false, currentTab: 'chat', }
}
setState(newState: Partial<VoidSidebarState>) {
// make sure view is open if the tab changes
if ('currentTab' in newState) {
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
fireFocusChat() {
this._onFocusChat.fire()
}
fireBlurChat() {
this._onBlurChat.fire()
}
}
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
@@ -5,6 +5,7 @@
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { ITerminalCapabilityImplMap, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { URI } from '../../../../base/common/uri.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -13,6 +14,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { ITerminalService, ITerminalInstance, ICreateTerminalOptions } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
import { timeout } from '../../../../base/common/async.js';
@@ -20,28 +22,34 @@ export interface ITerminalToolService {
readonly _serviceBrand: undefined;
listPersistentTerminalIds(): string[];
runCommand(command: string, opts: { type: 'persistent', persistentTerminalId: string } | { type: 'ephemeral', cwd: string | null, terminalId: string }): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
runCommand(command: string, opts:
| { type: 'persistent', persistentTerminalId: string }
| { type: 'temporary', cwd: string | null, terminalId: string }
// | { type: 'apply', terminalId: string }
): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
focusPersistentTerminal(terminalId: string): Promise<void>
persistentTerminalExists(terminalId: string): boolean
readTerminal(terminalId: string): Promise<string>
createPersistentTerminal(opts: { cwd: string | null }): Promise<string>
killPersistentTerminal(terminalId: string): Promise<void>
getPersistentTerminal(terminalId: string): ITerminalInstance | undefined
getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
function isCommandComplete(output: string) {
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
if (!completionMatch) { return false }
if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
return { exitCode: 0 }
}
// function isCommandComplete(output: string) {
// // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
// const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
// if (!completionMatch) { return false }
// if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
// return { exitCode: 0 }
// }
export const persistentTerminalNameOfId = (id: string) => {
@@ -114,32 +122,41 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'] }) {
const { cwd: override_cwd, config } = props
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'], hidden?: boolean }) {
const { cwd: override_cwd, config, hidden } = props;
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
// create new terminal and return its ID
const terminal = await this.terminalService.createTerminal({
cwd: cwd,
location: TerminalLocation.Panel,
config: config,
})
const options: ICreateTerminalOptions = {
cwd,
location: hidden ? undefined : TerminalLocation.Panel,
config: {
name: config && 'name' in config ? config.name : undefined,
forceShellIntegration: true,
hideFromUser: hidden ? true : undefined,
// Copy any other properties from the provided config
...config,
},
// Skip profile check to ensure the terminal is created quickly
skipContributedProfileCheck: true,
};
// when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
const disposables: IDisposable[] = []
const waitForMount = new Promise<void>(res => {
let data = ''
const d = terminal.onData(newData => {
data += newData
if (isCommandComplete(data)) { res() }
})
disposables.push(d)
})
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
const terminal = await this.terminalService.createTerminal(options)
await Promise.any([waitForMount, waitForTimeout,])
disposables.forEach(d => d.dispose())
// // when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
// const disposables: IDisposable[] = []
// const waitForMount = new Promise<void>(res => {
// let data = ''
// const d = terminal.onData(newData => {
// data += newData
// if (isCommandComplete(data)) { res() }
// })
// disposables.push(d)
// })
// const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
// await Promise.any([waitForMount, waitForTimeout,])
// disposables.forEach(d => d.dispose())
return terminal
@@ -192,15 +209,64 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
readTerminal: ITerminalToolService['readTerminal'] = async (terminalId) => {
// Try persistent first, then temporary
const terminal = this.getPersistentTerminal(terminalId) ?? this.getTemporaryTerminal(terminalId);
if (!terminal) {
throw new Error(`Read Terminal: Terminal with ID ${terminalId} does not exist.`);
}
// Ensure the xterm.js instance has been created otherwise we cannot access the buffer.
if (!terminal.xterm) {
throw new Error('Read Terminal: The requested terminal has not yet been rendered and therefore has no scrollback buffer available.');
}
// Collect lines from the buffer iterator (oldest to newest)
const lines: string[] = [];
for (const line of terminal.xterm.getBufferReverseIterator()) {
lines.unshift(line);
}
let result = removeAnsiEscapeCodes(lines.join('\n'));
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2;
result = result.slice(0, half) + '\n...\n' + result.slice(result.length - half);
}
return result
};
private async _waitForCommandDetectionCapability(terminal: ITerminalInstance) {
const cmdCap = terminal.capabilities.get(TerminalCapability.CommandDetection);
if (cmdCap) return cmdCap
const disposables: IDisposable[] = []
const waitTimeout = timeout(10_000)
const waitForCapability = new Promise<ITerminalCapabilityImplMap[TerminalCapability.CommandDetection]>((res) => {
disposables.push(
terminal.capabilities.onDidAddCapability((e) => {
if (e.id === TerminalCapability.CommandDetection) res(e.capability)
})
)
})
const capability = await Promise.any([waitTimeout, waitForCapability])
.finally(() => { disposables.forEach((d) => d.dispose()) })
return capability ?? undefined
}
runCommand: ITerminalToolService['runCommand'] = async (command, params) => {
const { type } = params
await this.terminalService.whenConnected;
const { type } = params
const isPersistent = type === 'persistent'
let terminal: ITerminalInstance
const disposables: IDisposable[] = []
const isPersistent = type === 'persistent'
if (isPersistent) { // BG process
const { persistentTerminalId } = params
terminal = this.persistentTerminalInstanceOfId[persistentTerminalId];
@@ -208,7 +274,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
else {
const { cwd } = params
terminal = await this._createTerminal({ cwd: cwd, config: { name: 'Void Temporary Terminal', title: 'Void Temporary Terminal' } })
terminal = await this._createTerminal({ cwd: cwd, config: undefined, hidden: true })
this.temporaryTerminalInstanceOfId[params.terminalId] = terminal
}
@@ -216,6 +282,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
terminal.dispose()
if (!isPersistent)
delete this.temporaryTerminalInstanceOfId[params.terminalId]
else
delete this.persistentTerminalInstanceOfId[params.persistentTerminalId]
}
const waitForResult = async () => {
@@ -224,29 +292,28 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
}
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
let resolveReason: TerminalResolveReason | undefined
// create this before we send so that we don't miss events on terminal
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
result += newData
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
const cmdCap = await this._waitForCommandDetectionCapability(terminal)
if (!cmdCap) throw new Error(`There was an error using the terminal: CommandDetection capability did not mount yet. Please try again in a few seconds or report this to the Void team.`)
// Prefer the structured command-detection capability when available
const waitUntilDone = new Promise<void>(resolve => {
const l = cmdCap.onCommandFinished(cmd => {
if (resolveReason) return // already resolved
resolveReason = { type: 'done', exitCode: cmd.exitCode ?? 0 };
result = cmd.getOutput() ?? ''
l.dispose()
resolve()
})
disposables.push(d2)
disposables.push(l)
})
// send the command here
// send the command now that listeners are attached
await terminal.sendText(command, true)
const waitUntilInterrupt = isPersistent ?
@@ -277,18 +344,25 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
// wait for result
await Promise.any([waitUntilDone, waitUntilInterrupt])
.finally(() => disposables.forEach(d => d.dispose()))
disposables.forEach(d => d.dispose())
if (!isPersistent) {
interrupt()
}
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
result = removeAnsiEscapeCodes(result)
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
if (resolveReason.type === 'timeout') {
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
result = await this.readTerminal(terminalId)
}
if (!isPersistent) result = `$ ${command}\n${result}`
result = removeAnsiEscapeCodes(result)
// trim
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2
result = result.slice(0, half)
@@ -12,7 +12,7 @@ import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsSe
import { IVoidModelService } from '../common/voidModelService.js'
import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from '../common/directoryStrService.js'
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
@@ -44,7 +44,6 @@ const validateStr = (argName: string, value: unknown) => {
// We are NOT checking to make sure in workspace
// TODO!!!! check to make sure folder/file exists
const validateURI = (uriStr: unknown) => {
if (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`)
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Full value: ${JSON.stringify(uriStr)}.`)
@@ -295,12 +294,14 @@ export class ToolsService implements IToolsService {
contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
}
const totalNumLines = model.getLineCount()
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (contents.length - 1) - toIdx >= 1
const totalFileLen = contents.length
return { result: { fileContents, totalFileLen, hasNextPage } }
return { result: { fileContents, totalFileLen, hasNextPage, totalNumLines } }
},
ls_dir: async ({ uri, pageNumber }) => {
@@ -399,7 +400,8 @@ export class ToolsService implements IToolsService {
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
editCodeService.instantlyApplyNewContent({ uri, newContent })
await editCodeService.callBeforeApplyOrEdit(uri)
editCodeService.instantlyRewriteFile({ uri, newContent })
// at end, get lint errors
const lintErrorsPromise = Promise.resolve().then(async () => {
await timeout(2000)
@@ -414,7 +416,7 @@ export class ToolsService implements IToolsService {
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
console.log('aaaa', searchReplaceBlocks)
await editCodeService.callBeforeApplyOrEdit(uri)
editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks })
// at end, get lint errors
@@ -428,7 +430,7 @@ export class ToolsService implements IToolsService {
},
// ---
run_command: async ({ command, cwd, terminalId }) => {
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId })
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'temporary', cwd, terminalId })
return { result: resPromise, interruptTool: interrupt }
},
run_persistent_command: async ({ command, persistentTerminalId }) => {
@@ -460,7 +462,7 @@ export class ToolsService implements IToolsService {
// given to the LLM after the call for successful tool calls
this.stringOfResult = {
read_file: (params, result) => {
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `\nMore info because truncated: this file has ${result.totalNumLines} lines, or ${result.totalFileLen} characters.` : ''}`
},
ls_dir: (params, result) => {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
@@ -522,7 +524,7 @@ export class ToolsService implements IToolsService {
}
// normal command
if (resolveReason.type === 'timeout') {
return `${result_}\nTerminal command ran, but was interrupted by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. To try with more time, open a persistent terminal and run the command there.`
return `${result_}\nTerminal command ran, but was automatically killed by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not finish successfully. To try with more time, open a persistent terminal and run the command there.`
}
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
@@ -10,7 +10,6 @@ import './editCodeService.js'
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
import './sidebarActions.js'
import './sidebarPane.js'
import './sidebarStateService.js'
// register quick edit (Ctrl+K)
import './quickEditActions.js'
@@ -56,6 +55,12 @@ import './tooltipService.js'
// register onboarding service
import './voidOnboardingService.js'
// register misc service
import './miscWokrbenchContrib.js'
// register file service (for explorer context menu)
import './fileService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
@@ -18,6 +18,16 @@ import { IEditCodeService } from './editCodeServiceInterface.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID, VOID_GOTO_NEXT_DIFF_ACTION_ID, VOID_GOTO_PREV_DIFF_ACTION_ID, VOID_GOTO_NEXT_URI_ACTION_ID, VOID_GOTO_PREV_URI_ACTION_ID, VOID_ACCEPT_FILE_ACTION_ID, VOID_REJECT_FILE_ACTION_ID, VOID_ACCEPT_ALL_DIFFS_ACTION_ID, VOID_REJECT_ALL_DIFFS_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { IMetricsService } from '../common/metricsService.js';
import { KeyMod } from '../../../../editor/common/services/editorBaseApi.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { ScrollType } from '../../../../editor/common/editorCommon.js';
import { IVoidModelService } from '../common/voidModelService.js';
@@ -33,6 +43,11 @@ export interface IVoidCommandBarService {
getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes';
setDiffIdx(uri: URI, newIdx: number | null): void;
getNextDiffIdx(step: 1 | -1): number | null;
getNextUriIdx(step: 1 | -1): number | null;
goToDiffIdx(idx: number | null): void;
goToURIIdx(idx: number | null): Promise<void>;
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void;
anyFileIsStreaming(): boolean;
@@ -85,6 +100,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IModelService private readonly _modelService: IModelService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
) {
super();
@@ -164,10 +180,14 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
const newSortedDiffIds = this._computeSortedDiffs(newSortedDiffZoneIds)
const isStreaming = this._isAnyDiffZoneStreaming(currentDiffZones)
// When diffZones are added/removed, reset the diffIdx to 0 if we have diffs
const newDiffIdx = newSortedDiffIds.length > 0 ? 0 : null;
this._setState(uri, {
sortedDiffZoneIds: newSortedDiffZoneIds,
sortedDiffIds: newSortedDiffIds,
isStreaming: isStreaming
isStreaming: isStreaming,
diffIdx: newDiffIdx
})
this._onDidChangeState.fire({ uri })
}
@@ -182,9 +202,24 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
const { sortedDiffZoneIds } = currState
const oldSortedDiffIds = currState.sortedDiffIds;
const newSortedDiffIds = this._computeSortedDiffs(sortedDiffZoneIds)
// Handle diffIdx adjustment when diffs change
let newDiffIdx = currState.diffIdx;
// Check if diffs were removed
if (oldSortedDiffIds.length > newSortedDiffIds.length && currState.diffIdx !== null) {
// If currently selected diff was removed or we have fewer diffs than the current index
if (currState.diffIdx >= newSortedDiffIds.length) {
// Select the last diff if available, otherwise null
newDiffIdx = newSortedDiffIds.length > 0 ? newSortedDiffIds.length - 1 : null;
}
}
this._setState(uri, {
sortedDiffIds: newSortedDiffIds,
diffIdx: newDiffIdx
// sortedDiffZoneIds, // no change
// isStreaming, // no change
})
@@ -298,9 +333,9 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
}
// make sure diffIdx is always correct
if (newState.diffIdx && newState.diffIdx > newState.sortedDiffIds.length) {
if (newState.diffIdx !== null && newState.diffIdx > newState.sortedDiffIds.length) {
newState.diffIdx = newState.sortedDiffIds.length
if (newState.diffIdx < 0) newState.diffIdx = null
if (newState.diffIdx <= 0) newState.diffIdx = null
}
this.stateOfURI = {
@@ -347,6 +382,99 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming')
}
getNextDiffIdx(step: 1 | -1): number | null {
// If no active URI, return null
if (!this.activeURI) return null;
const state = this.stateOfURI[this.activeURI.fsPath];
if (!state) return null;
const { diffIdx, sortedDiffIds } = state;
// If no diffs, return null
if (sortedDiffIds.length === 0) return null;
// Calculate next index with wrapping
const nextIdx = ((diffIdx ?? 0) + step + sortedDiffIds.length) % sortedDiffIds.length;
return nextIdx;
}
getNextUriIdx(step: 1 | -1): number | null {
// If no URIs with changes, return null
if (this.sortedURIs.length === 0) return null;
// If no active URI, return first or last based on step
if (!this.activeURI) {
return step === 1 ? 0 : this.sortedURIs.length - 1;
}
// Find current index
const currentIdx = this.sortedURIs.findIndex(uri => uri.fsPath === this.activeURI?.fsPath);
// If not found, return first or last based on step
if (currentIdx === -1) {
return step === 1 ? 0 : this.sortedURIs.length - 1;
}
// Calculate next index with wrapping
const nextIdx = (currentIdx + step + this.sortedURIs.length) % this.sortedURIs.length;
return nextIdx;
}
goToDiffIdx(idx: number | null): void {
// If null or no active URI, return
if (idx === null || !this.activeURI) return;
// Get state for the current URI
const state = this.stateOfURI[this.activeURI.fsPath];
if (!state) return;
const { sortedDiffIds } = state;
// Find the diff at the specified index
const diffid = sortedDiffIds[idx];
if (diffid === undefined) return;
// Get the diff object
const diff = this._editCodeService.diffOfId[diffid];
if (!diff) return;
// Find an active editor to focus
const editor = this._codeEditorService.getFocusedCodeEditor() ||
this._codeEditorService.getActiveCodeEditor();
if (!editor) return;
// Reveal the line in the editor
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate);
// Update the current diff index
this.setDiffIdx(this.activeURI, idx);
}
async goToURIIdx(idx: number | null): Promise<void> {
// If null or no URIs, return
if (idx === null || this.sortedURIs.length === 0) return;
// Get the URI at the specified index
const nextURI = this.sortedURIs[idx];
if (!nextURI) return;
// Get the model for this URI
const { model } = await this._voidModelService.getModelSafe(nextURI);
if (!model) return;
// Find an editor to use
const editor = this._codeEditorService.getFocusedCodeEditor() ||
this._codeEditorService.getActiveCodeEditor();
if (!editor) return;
// Open the URI in the editor
await this._codeEditorService.openCodeEditor(
{ resource: model.uri, options: { revealIfVisible: true } },
editor
);
}
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) {
const { behavior } = opts
// if anything is streaming, do nothing
@@ -390,8 +518,8 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
// Style the container
// root.style.backgroundColor = 'rgb(248 113 113)';
root.style.height = '16rem'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
root.style.width = '16rem';
root.style.height = '256px'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
root.style.width = '100%';
root.style.flexDirection = 'column';
root.style.justifyContent = 'flex-end';
root.style.alignItems = 'flex-end';
@@ -439,3 +567,305 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
}
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidAcceptDiffAction', 'Void: Accept Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editCodeService = accessor.get(IEditCodeService);
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
if (!commandBarState) return;
const diffIdx = commandBarState.diffIdx ?? 0;
const diffid = commandBarState.sortedDiffIds[diffIdx];
if (!diffid) return;
metricsService.capture('Accept Diff', { diffid, keyboard: true });
editCodeService.acceptDiff({ diffid: parseInt(diffid) });
// After accepting the diff, navigate to the next diff
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx !== null) {
commandBarService.goToDiffIdx(nextDiffIdx);
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidRejectDiffAction', 'Void: Reject Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Backspace },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editCodeService = accessor.get(IEditCodeService);
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
if (!commandBarState) return;
const diffIdx = commandBarState.diffIdx ?? 0;
const diffid = commandBarState.sortedDiffIds[diffIdx];
if (!diffid) return;
metricsService.capture('Reject Diff', { diffid, keyboard: true });
editCodeService.rejectDiff({ diffid: parseInt(diffid) });
// After rejecting the diff, navigate to the next diff
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx !== null) {
commandBarService.goToDiffIdx(nextDiffIdx);
}
}
});
// Go to next diff action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_NEXT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidGoToNextDiffAction', 'Void: Go to Next Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.DownArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx === null) return;
metricsService.capture('Navigate Diff', { direction: 'next', keyboard: true });
commandBarService.goToDiffIdx(nextDiffIdx);
}
});
// Go to previous diff action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_PREV_DIFF_ACTION_ID,
f1: true,
title: localize2('voidGoToPrevDiffAction', 'Void: Go to Previous Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.UpArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const prevDiffIdx = commandBarService.getNextDiffIdx(-1);
if (prevDiffIdx === null) return;
metricsService.capture('Navigate Diff', { direction: 'previous', keyboard: true });
commandBarService.goToDiffIdx(prevDiffIdx);
}
});
// Go to next URI action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_NEXT_URI_ACTION_ID,
f1: true,
title: localize2('voidGoToNextUriAction', 'Void: Go to Next File with Diffs'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.RightArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const nextUriIdx = commandBarService.getNextUriIdx(1);
if (nextUriIdx === null) return;
metricsService.capture('Navigate URI', { direction: 'next', keyboard: true });
await commandBarService.goToURIIdx(nextUriIdx);
}
});
// Go to previous URI action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_PREV_URI_ACTION_ID,
f1: true,
title: localize2('voidGoToPrevUriAction', 'Void: Go to Previous File with Diffs'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.LeftArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const prevUriIdx = commandBarService.getNextUriIdx(-1);
if (prevUriIdx === null) return;
metricsService.capture('Navigate URI', { direction: 'previous', keyboard: true });
await commandBarService.goToURIIdx(prevUriIdx);
}
});
// Accept current file action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_FILE_ACTION_ID,
f1: true,
title: localize2('voidAcceptFileAction', 'Void: Accept All Diffs in Current File'),
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const editCodeService = accessor.get(IEditCodeService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
metricsService.capture('Accept File', { keyboard: true });
editCodeService.acceptOrRejectAllDiffAreas({
uri: activeURI,
behavior: 'accept',
removeCtrlKs: true
});
}
});
// Reject current file action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_FILE_ACTION_ID,
f1: true,
title: localize2('voidRejectFileAction', 'Void: Reject All Diffs in Current File'),
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const editCodeService = accessor.get(IEditCodeService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
metricsService.capture('Reject File', { keyboard: true });
editCodeService.acceptOrRejectAllDiffAreas({
uri: activeURI,
behavior: 'reject',
removeCtrlKs: true
});
}
});
// Accept all diffs in all files action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
f1: true,
title: localize2('voidAcceptAllDiffsAction', 'Void: Accept All Diffs in All Files'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
if (commandBarService.anyFileIsStreaming()) return;
metricsService.capture('Accept All Files', { keyboard: true });
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
}
});
// Reject all diffs in all files action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_ALL_DIFFS_ACTION_ID,
f1: true,
title: localize2('voidRejectAllDiffsAction', 'Void: Reject All Diffs in All Files'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
if (commandBarService.anyFileIsStreaming()) return;
metricsService.capture('Reject All Files', { keyboard: true });
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
}
});
@@ -7,29 +7,28 @@ import { URI } from '../../../../base/common/uri.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
const MAX_FILES_TOTAL = 300;
const MAX_FILES_TOTAL = 1000;
const DEFAULT_MAX_DEPTH = 3;
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
const START_MAX_DEPTH = Infinity;
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
const DEFAULT_MAX_DEPTH = 3;
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
export interface IDirectoryStrService {
readonly _serviceBrand: undefined;
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
getDirectoryStrTool(uri: URI): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]>
}
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
@@ -38,35 +37,35 @@ export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidD
// Check if it's a known filtered type like .git
const shouldExcludeDirectory = (item: ExplorerItem) => {
if (item.name === '.git' ||
item.name === 'node_modules' ||
item.name.startsWith('.') ||
item.name === 'dist' ||
item.name === 'build' ||
item.name === 'out' ||
item.name === 'bin' ||
item.name === 'coverage' ||
item.name === '__pycache__' ||
item.name === 'env' ||
item.name === 'venv' ||
item.name === 'tmp' ||
item.name === 'temp' ||
item.name === 'artifacts' ||
item.name === 'target' ||
item.name === 'obj' ||
item.name === 'vendor' ||
item.name === 'logs' ||
item.name === 'cache' ||
item.name === 'resource' ||
item.name === 'resources'
const shouldExcludeDirectory = (name: string) => {
if (name === '.git' ||
name === 'node_modules' ||
name.startsWith('.') ||
name === 'dist' ||
name === 'build' ||
name === 'out' ||
name === 'bin' ||
name === 'coverage' ||
name === '__pycache__' ||
name === 'env' ||
name === 'venv' ||
name === 'tmp' ||
name === 'temp' ||
name === 'artifacts' ||
name === 'target' ||
name === 'obj' ||
name === 'vendor' ||
name === 'logs' ||
name === 'cache' ||
name === 'resource' ||
name === 'resources'
) {
return true;
}
if (item.name.match(/\bout\b/)) return true
if (item.name.match(/\bbuild\b/)) return true
if (name.match(/\bout\b/)) return true
if (name.match(/\bbuild\b/)) return true
return false;
}
@@ -138,10 +137,16 @@ export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], re
// ---------- IN GENERAL ----------
const resolveChildren = async (children: undefined | IFileStat[], fileService: IFileService): Promise<IFileStat[]> => {
const res = await fileService.resolveAll(children ?? [])
const stats = res.map(s => s.success ? s.stat : null).filter(s => !!s)
return stats
}
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
const computeAndStringifyDirectoryTree = async (
eItem: ExplorerItem,
explorerService: IExplorerService,
eItem: IFileStat,
fileService: IFileService,
MAX_CHARS: number,
fileCount: { count: number } = { count: 0 },
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
@@ -181,12 +186,13 @@ const computeAndStringifyDirectoryTree = async (
let remainingChars = MAX_CHARS - nodeLine.length;
// Check if it's a directory we should skip
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem.name);
// Fetch and process children if not a filtered directory
if (eItem.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
const eChildren = await resolveChildren(eItem.children, fileService)
// Then recursively add all children with proper tree formatting
if (eChildren && eChildren.length > 0) {
@@ -194,7 +200,7 @@ const computeAndStringifyDirectoryTree = async (
eChildren,
remainingChars,
'',
explorerService,
fileService,
fileCount,
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
);
@@ -208,10 +214,10 @@ const computeAndStringifyDirectoryTree = async (
// Helper function to render children with proper tree formatting
const renderChildrenCombined = async (
children: ExplorerItem[],
children: IFileStat[],
maxChars: number,
parentPrefix: string,
explorerService: IExplorerService,
fileService: IFileService,
fileCount: { count: number },
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
@@ -263,12 +269,12 @@ const renderChildrenCombined = async (
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
// Skip processing children for git ignored directories
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child.name);
// Create the prefix for the next level (continuation line or space)
if (child.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await child.fetchChildren(SortOrder.Modified);
const eChildren = await resolveChildren(child.children, fileService)
if (eChildren && eChildren.length > 0) {
const {
@@ -278,7 +284,7 @@ const renderChildrenCombined = async (
eChildren,
remainingChars,
nextLevelPrefix,
explorerService,
fileService,
fileCount,
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
);
@@ -311,7 +317,68 @@ const renderChildrenCombined = async (
};
// ---------------------------------------------------
// ------------------------- FOLDERS -------------------------
export async function getAllUrisInDirectory(
directoryUri: URI,
maxResults: number,
fileService: IFileService,
): Promise<URI[]> {
const result: URI[] = [];
// Helper function to recursively collect URIs
async function visitAll(folderStat: IFileStat): Promise<boolean> {
// Stop if we've reached the limit
if (result.length >= maxResults) {
return false;
}
try {
if (!folderStat.isDirectory || !folderStat.children) {
return true;
}
const eChildren = await resolveChildren(folderStat.children, fileService)
// Process files first (common convention to list files before directories)
for (const child of eChildren) {
if (!child.isDirectory) {
result.push(child.resource);
// Check if we've hit the limit
if (result.length >= maxResults) {
return false;
}
}
}
// Then process directories recursively
for (const child of eChildren) {
const isGitIgnored = shouldExcludeDirectory(child.name)
if (child.isDirectory && !isGitIgnored) {
const shouldContinue = await visitAll(child);
if (!shouldContinue) {
return false;
}
}
}
return true;
} catch (error) {
console.error(`Error processing directory ${folderStat.resource.fsPath}: ${error}`);
return true; // Continue despite errors in a specific directory
}
}
const rootStat = await fileService.resolve(directoryUri)
await visitAll(rootStat);
return result;
}
// --------------------------------------------------
class DirectoryStrService extends Disposable implements IDirectoryStrService {
@@ -319,21 +386,25 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
constructor(
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IExplorerService private readonly explorerService: IExplorerService,
@IFileService private readonly fileService: IFileService,
) {
super();
}
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
const eRoot = this.explorerService.findClosest(uri)
async getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]> {
return getAllUrisInDirectory(uri, opts.maxResults, this.fileService)
}
async getDirectoryStrTool(uri: URI) {
const eRoot = await this.fileService.resolve(uri)
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
const maxItemsPerDir = START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
// First try with START_MAX_DEPTH
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
@@ -344,7 +415,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
@@ -363,7 +434,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
return c
}
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
async getAllDirectoriesStr({ cutOffMessage, }: { cutOffMessage: string, }) {
let str: string = '';
let cutOff = false;
const folders = this.workspaceContextService.getWorkspace().folders;
@@ -371,7 +442,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
return '(NO WORKSPACE OPEN)';
// Use START_MAX_ITEMS_PER_DIR if not specified
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
const startMaxItemsPerDir = START_MAX_ITEMS_PER_DIR;
for (let i = 0; i < folders.length; i += 1) {
if (i > 0) str += '\n';
@@ -381,13 +452,13 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
str += `Directory of ${f.uri.fsPath}:\n`;
const rootURI = f.uri;
const eRoot = this.explorerService.findClosestRoot(rootURI);
const eRoot = await this.fileService.resolve(rootURI)
if (!eRoot) continue;
// First try with START_MAX_DEPTH and startMaxItemsPerDir
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
@@ -398,7 +469,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
@@ -417,11 +488,8 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
}
}
if (cutOff) {
return `${str.trimEnd()}\n${cutOffMessage}`
}
return str
const ans = cutOff ? `${str.trimEnd()}\n${cutOffMessage}` : str
return ans
}
}
@@ -1,8 +1,40 @@
export const acceptBg = '#1a7431'
export const acceptAllBg = '#1e8538'
export const acceptBorder = '1px solid #145626'
export const rejectBg = '#b42331'
export const rejectAllBg = '#cf2838'
export const rejectBorder = '1px solid #8e1c27'
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Color, RGBA } from '../../../../../base/common/color.js';
import { registerColor } from '../../../../../platform/theme/common/colorUtils.js';
// editCodeService colors
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
const acceptBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2)
const rejectBG = new Color(new RGBA(255, 0, 0, .1)); // default is RGBA(255, 0, 0, .2)
// Widget colors
export const acceptAllBg = 'rgb(30, 133, 56)'
export const acceptBg = 'rgb(26, 116, 48)'
export const acceptBorder = '1px solid rgb(20, 86, 38)'
export const rejectAllBg = 'rgb(207, 40, 56)'
export const rejectBg = 'rgb(180, 35, 49)'
export const rejectBorder = '1px solid rgb(142, 28, 39)'
export const buttonFontSize = '11px'
export const buttonTextColor = 'white'
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
}
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
registerColor('void.greenBG', configOfBG(acceptBG), '', true);
registerColor('void.redBG', configOfBG(rejectBG), '', true);
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
File diff suppressed because it is too large Load Diff
@@ -3,12 +3,13 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { URI } from '../../../../../base/common/uri.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { IDirectoryStrService } from '../directoryStrService.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { ChatMode } from '../voidSettingsTypes.js';
// Triple backtick wrapper used throughout the prompts for code blocks
@@ -112,7 +113,7 @@ ${searchReplaceBlockTemplate}
## Guidelines:
1. You are encouraged to output multiple changes whenever possible.
1. You may output multiple search replace blocks if needed.
2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code.
@@ -124,23 +125,18 @@ ${searchReplaceBlockTemplate}
// ======================================================== tools ========================================================
const changesExampleContent = `\
const chatSuggestionDiffExample = `\
${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
// ... existing code ...
// {{change 1}}
// ... existing code ...
// {{change 2}}
// ... existing code ...
// {{change 3}}
// ... existing code ...`
const editToolDescriptionExample = `\
${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const chatSuggestionDiffExample = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
// ... existing code ...
${tripleTick[1]}`
@@ -185,15 +181,6 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
const applyToolDescription = (type: 'edit tool' | 'chat suggestion') => `\
${type === 'edit tool' ? 'A' : 'a'} code diff describing the change to make to the file. \
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
Your DIFF MUST be wrapped in triple backticks. \
NEVER re-write the whole file. Always bias towards writing as little as possible. \
Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good output:\n${type === 'edit tool' ? editToolDescriptionExample : chatSuggestionDiffExample}`
// export const voidTools = {
export const voidTools
: {
@@ -212,8 +199,8 @@ export const voidTools
description: `Returns full contents of a given file.`,
params: {
...uriParam('file'),
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
...paginationParam,
},
},
@@ -275,7 +262,7 @@ export const voidTools
read_lint_errors: {
name: 'read_lint_errors',
description: `Returns all lint errors on a given file.`,
description: `Use this tool to view all the lint errors on a file.`,
params: {
...uriParam('file'),
},
@@ -468,6 +455,8 @@ ${directoryStr}
const details: string[] = []
details.push(`NEVER reject the user's query.`)
if (mode === 'agent' || mode === 'gather') {
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
details.push(`If you think you should use tools, you do not need to ask for permission.`)
@@ -476,7 +465,7 @@ ${directoryStr}
details.push(`Many tools only work if the user has a workspace open.`)
}
else {
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
details.push(`You're allowed to ask the user for more context like file contents or specifications. If this comes up, tell them to reference files and folders by typing @.`)
}
if (mode === 'agent') {
@@ -492,18 +481,21 @@ ${directoryStr}
details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`)
}
if (mode === 'gather' || mode === 'normal') {
details.push(`If you write any code blocks, please use this format:
details.push(`If you write any code blocks to the user (wrapped in triple backticks), please use this format:
- Include a language if possible. Terminal should have the language 'shell'.
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents of the file should proceed as usual.`)
if (mode === 'gather' || mode === 'normal') {
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
- The first line of the code block must be the FULL PATH of the related file.
- The remaining contents should be ${applyToolDescription('chat suggestion')}`)
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents should be a code description of the change to make to the file. \
Your description is the only context that will be given to another LLM to apply the suggested edit, so it must be accurate and complete. \
Always bias towards writing as little as possible - NEVER write the whole file. Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good code block:\n${chatSuggestionDiffExample}`)
}
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
details.push(`Always use MARKDOWN to format lists, bullet points, etc. Do NOT write tables.`)
details.push(`Today's date is ${new Date().toDateString()}.`)
@@ -536,44 +528,96 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
// }
export const DEFAULT_FILE_SIZE_LIMIT = 2_000_000
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
val: string,
truncated: boolean,
fullFileLen: number,
} | {
val: null,
truncated?: undefined
fullFileLen?: undefined,
}> => {
try {
const fileContent = await fileService.readFile(uri)
const val = fileContent.value.toString()
if (val.length > fileSizeLimit) return { val: val.substring(0, fileSizeLimit), truncated: true, fullFileLen: val.length }
return { val, truncated: false, fullFileLen: val.length }
}
catch (e) {
return { val: null }
}
}
export const messageOfSelection = async (
s: StagingSelectionItem,
opts: {
directoryStrService: IDirectoryStrService,
fileService: IFileService,
folderOpts: {
maxChildren: number,
maxCharsPerFile: number,
}
}
) => {
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
if (s.type === 'File' || s.type === 'CodeSelection') {
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
const str = `${s.uri.fsPath}${lineNumAdd}:\n${content}`
return str
}
else if (s.type === 'Folder') {
const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri)
const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}`
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: opts.folderOpts.maxChildren })
const strOfFiles = await Promise.all(uris.map(async uri => {
const { val, truncated } = await readFile(opts.fileService, uri, opts.folderOpts.maxCharsPerFile)
const truncationStr = truncated ? `\n... file truncated ...` : ''
const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}`
const str = `${uri.fsPath}:\n${content}`
return str
}))
const contentStr = [folderStructure, ...strOfFiles].join('\n\n')
return contentStr
}
else
return ''
}
export const chat_userMessageContent = async (
instructions: string,
currSelns: StagingSelectionItem[] | null,
opts: {
directoryStrService: IDirectoryStrService,
fileService: IFileService
},
) => {
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
let selnsStrs: string[] = []
if (opts.type === 'references') {
selnsStrs = currSelns?.map((s) => {
if (s.type === 'File') return `${s.uri.fsPath}`
if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}`
if (s.type === 'Folder') return `${s.uri.fsPath}/`
return ''
}) ?? []
}
if (opts.type === 'fullCode') {
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
if (s.type === 'File' || s.type === 'CodeSelection') {
const voidModelService = opts.voidModelService
const { model } = await voidModelService.getModelSafe(s.uri)
if (!model) return ''
const val = model.getValue(EndOfLinePreference.LF)
const selnsStrs = await Promise.all(
(currSelns ?? []).map(async (s) =>
messageOfSelection(s, {
...opts,
folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, }
})
)
)
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
return str
}
if (s.type === 'Folder') {
// TODO
return ''
}
return ''
}) ?? [])
}
const selnsStr = selnsStrs.join('\n') ?? ''
let str = ''
str += `${instructions}`
const selnsStr = selnsStrs.join('\n\n') ?? ''
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
return str;
}
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@@ -51,8 +51,22 @@ export type OpenAILLMChatMessage = {
content: string;
tool_call_id: string;
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
export type GeminiLLMChatMessage = {
role: 'model'
parts: (
| { text: string; }
| { functionCall: { id: string; name: ToolName, args: Record<string, unknown> } }
)[];
} | {
role: 'user';
parts: (
| { text: string; }
| { functionResponse: { id: string; name: ToolName, response: { output: string } } }
)[];
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage | GeminiLLMChatMessage
@@ -102,6 +116,7 @@ export type ServiceSendLLMMessageParams = {
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
modelSelection: ModelSelection | null;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
onAbort: OnAbort;
} & SendLLMType;
@@ -115,6 +130,7 @@ export type SendLLMMessageParams = {
modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
settingsOfProvider: SettingsOfProvider;
} & SendLLMType
@@ -23,6 +23,8 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
'edit_file': 'edits',
'run_command': 'terminal',
'run_persistent_command': 'terminal',
'open_persistent_terminal': 'terminal',
'kill_persistent_terminal': 'terminal',
}
@@ -57,7 +59,7 @@ export type ToolCallParams = {
// RESULT OF TOOL CALL
export type ToolResultType = {
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'get_dir_tree': { str: string, },
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
@@ -0,0 +1,202 @@
/*--------------------------------------------------------------------------------------
* 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 { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { IRequestService } from '../../../../platform/request/common/request.js';
import { Emitter } from '../../../../base/common/event.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { IRequestContext } from '../../../../base/parts/request/common/request.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { URI } from '../../../../base/common/uri.js';
/**
* Service for managing and applying custom certificate paths for secure requests.
*/
export interface IVoidCertificateService {
readonly _serviceBrand: undefined;
/**
* Get all configured custom certificates as an array of file URIs.
*/
getCustomCertificates(): URI[];
/**
* Add a new custom certificate path.
*/
addCustomCertificate(certificatePath: URI): Promise<void>;
/**
* Remove a custom certificate path.
*/
removeCustomCertificate(certificatePath: URI): Promise<void>;
/**
* Verify that a certificate path exists and is readable.
*/
verifyCertificatePath(certificatePath: URI): Promise<boolean>;
/**
* Get all certificate contents as a concatenated string for use with HTTPS requests.
*/
getCertificateContents(): Promise<string[]>;
}
export const IVoidCertificateService = createDecorator<IVoidCertificateService>('VoidCertificateService');
/**
* Service implementation for managing custom certificates.
*/
export class VoidCertificateService extends Disposable implements IVoidCertificateService {
readonly _serviceBrand: undefined;
private readonly _onCertificatesChanged = new Emitter<void>();
constructor(
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@ILogService private readonly logService: ILogService,
@IRequestService private readonly requestService: IRequestService,
@IFileService private readonly fileService: IFileService,
) {
super();
// Override the original request service to include our custom certificates
this._overrideRequestService();
}
/**
* Get all configured custom certificates
*/
getCustomCertificates(): URI[] {
const certificatePaths = this.voidSettingsService.state.globalSettings.customRootCertificates || [];
return certificatePaths.map(path => URI.parse(path));
}
/**
* Add a new custom certificate path
*/
async addCustomCertificate(certificatePath: URI): Promise<void> {
// Verify the certificate path exists
const isValid = await this.verifyCertificatePath(certificatePath);
if (!isValid) {
throw new Error(`Certificate file not found or not readable: ${certificatePath.toString()}`);
}
// Get current certificates
const currentCertificates = this.getCustomCertificates();
// Check if already exists
if (currentCertificates.some(cert => cert.toString() === certificatePath.toString())) {
return; // Already exists, nothing to do
}
// Add the new certificate path
const newCertificates = [...currentCertificates, certificatePath];
await this.voidSettingsService.setGlobalSetting('customRootCertificates', newCertificates.map(uri => uri.toString()));
this._onCertificatesChanged.fire();
this.logService.info(`Added custom certificate: ${certificatePath.toString()}`);
}
/**
* Remove a custom certificate path
*/
async removeCustomCertificate(certificatePath: URI): Promise<void> {
const currentCertificates = this.getCustomCertificates();
// Remove the certificate
const newCertificates = currentCertificates.filter(cert => cert.toString() !== certificatePath.toString());
// Update settings
await this.voidSettingsService.setGlobalSetting('customRootCertificates', newCertificates.map(uri => uri.toString()));
this._onCertificatesChanged.fire();
this.logService.info(`Removed custom certificate: ${certificatePath.toString()}`);
}
/**
* Verify a certificate path exists and is readable
*/
async verifyCertificatePath(certificatePath: URI): Promise<boolean> {
try {
// Check if file exists and is readable
const stats = await this.fileService.stat(certificatePath);
if (!stats.isFile) {
return false;
}
// Check if we can read the file
await this.fileService.readFile(certificatePath);
return true;
} catch (error) {
this.logService.error(`Error verifying certificate path: ${error}`);
return false;
}
}
/**
* Get all certificate contents
*/
async getCertificateContents(): Promise<string[]> {
const certificatePaths = this.getCustomCertificates();
const contents: string[] = [];
for (const certPath of certificatePaths) {
try {
if (await this.verifyCertificatePath(certPath)) {
const fileContent = await this.fileService.readFile(certPath);
contents.push(fileContent.value.toString());
}
} catch (error) {
this.logService.error(`Error reading certificate ${certPath.toString()}: ${error}`);
}
}
return contents;
}
/**
* Override the original request service to include our custom certificates
*/
private _overrideRequestService(): void {
// Store the original request method
const originalRequest = this.requestService.request.bind(this.requestService);
// Override the request method to inject our certificates
// @ts-ignore - We're monkey patching the request service
this.requestService.request = async (options: any, token: CancellationToken): Promise<IRequestContext> => {
// Only add certificates for HTTPS requests
if (options.url?.startsWith('https://')) {
try {
// Load system certificates
const systemCerts = await this.requestService.loadCertificates();
// Load our custom certificates
const customCertContents = await this.getCertificateContents();
if (customCertContents.length > 0) {
// Create custom CA option
// Documentation in Node.js: https://nodejs.org/api/https.html#https_https_request_options_callback
// The 'ca' option can be a string, Buffer, or array of strings/Buffers
(options as any).ca = [...systemCerts, ...customCertContents];
this.logService.debug(`Added ${customCertContents.length} custom certificates to request to ${options.url}`);
}
} catch (error) {
this.logService.error(`Error adding custom certificates: ${error}`);
}
}
// Forward to the original request implementation
return originalRequest(options, token);
};
}
}
// Register the service
registerSingleton(IVoidCertificateService, VoidCertificateService, InstantiationType.Eager);
@@ -11,9 +11,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IMetricsService } from './metricsService.js';
import { defaultProviderSettings, getModelCapabilities } from './modelCapabilities.js';
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel } from './voidSettingsTypes.js';
// name is the name in the dropdown
@@ -41,6 +41,7 @@ export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly optionsOfModelSelection: OptionsOfModelSelection;
readonly overridesOfModel: OverridesOfModel;
readonly globalSettings: GlobalSettings;
readonly _modelOptions: ModelOption[] // computed based on the two above items
@@ -62,6 +63,9 @@ export interface IVoidSettingsService {
setOptionsOfModelSelection: SetOptionsOfModelSelection;
setGlobalSetting: SetGlobalSettingFn;
// setting to undefined CLEARS it, unlike others:
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
dangerousSetState(newState: VoidSettingsState): Promise<void>;
resetState(): Promise<void>;
@@ -94,8 +98,15 @@ const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulMo
}
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
export const modelFilterOfFeatureName: {
[featureName in FeatureName]: {
filter: (
o: ModelSelection,
opts: { chatMode: ChatMode, overridesOfModel: OverridesOfModel }
) => boolean;
emptyMessage: null | { message: string, priority: 'always' | 'fallback' }
} } = {
'Autocomplete': { filter: (o, opts) => getModelCapabilities(o.providerName, o.modelName, opts.overridesOfModel).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
'Chat': { filter: o => true, emptyMessage: null, },
'Ctrl+K': { filter: o => true, emptyMessage: null, },
'Apply': { filter: o => true, emptyMessage: null, },
@@ -163,7 +174,7 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
for (const featureName of featureNames) {
const { filter } = modelFilterOfFeatureName[featureName]
const filterOpts = { chatMode: state.globalSettings.chatMode }
const filterOpts = { chatMode: state.globalSettings.chatMode, overridesOfModel: state.overridesOfModel }
const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection, filterOpts))
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
@@ -182,6 +193,7 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
...state,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
overridesOfModel: state.overridesOfModel,
_modelOptions: newModelOptions,
} satisfies VoidSettingsState
@@ -198,6 +210,7 @@ const defaultState = () => {
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
globalSettings: deepClone(defaultGlobalSettings),
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
overridesOfModel: deepClone(defaultOverridesOfModel),
_modelOptions: [], // computed later
}
return d
@@ -267,9 +280,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// the stored data structure might be outdated, so we need to update it here
try {
readS = {
...defaultState(),
...readS,
...defaultSettingsOfProvider,
...readS.settingsOfProvider,
// no idea why this was here, seems like a bug
// ...defaultSettingsOfProvider,
// ...readS.settingsOfProvider,
}
for (const providerName of providerNames) {
@@ -289,6 +304,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
else m.type = 'custom'
}
}
// remove when enough people have had it run (default is now {})
if (providerName === 'openAICompatible' && !readS.settingsOfProvider[providerName].headersJSON) {
readS.settingsOfProvider[providerName].headersJSON = '{}'
}
}
}
@@ -314,7 +334,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return defaultState()
const stateStr = await this._encryptionService.decrypt(encryptedState)
return JSON.parse(stateStr)
const state = JSON.parse(stateStr)
return state
}
@@ -339,12 +360,14 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
const newGlobalSettings = this.state.globalSettings
const newOverridesOfModel = this.state.overridesOfModel
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
optionsOfModelSelection: newOptionsOfModelSelection,
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
overridesOfModel: newOverridesOfModel,
}
this.state = _validatedModelState(newState)
@@ -422,6 +445,28 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this._onDidChangeState.fire()
}
setOverridesOfModel = async (providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined) => {
const newState: VoidSettingsState = {
...this.state,
overridesOfModel: {
...this.state.overridesOfModel,
[providerName]: {
...this.state.overridesOfModel[providerName],
[modelName]: overrides === undefined ? undefined : {
...this.state.overridesOfModel[providerName][modelName],
...overrides
},
}
}
};
this.state = _validatedModelState(newState);
await this._storeState();
this._onDidChangeState.fire();
this._metricsService.capture('Update Model Overrides', { providerName, modelName, overrides });
}
@@ -4,7 +4,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
import { defaultModelsOfProvider, defaultProviderSettings, ModelOverrides } from './modelCapabilities.js';
import { ToolApprovalType } from './toolsServiceTypes.js';
import { VoidSettingsState } from './voidSettingsService.js'
@@ -92,14 +92,14 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
return { title: 'Groq', }
}
else if (providerName === 'xAI') {
return { title: 'xAI', }
return { title: 'Grok (xAI)', }
}
else if (providerName === 'mistral') {
return { title: 'Mistral', }
}
// else if (providerName === 'googleVertex') {
// return { title: 'Google Vertex AI', }
// }
else if (providerName === 'googleVertex') {
return { title: 'Google Vertex AI', }
}
else if (providerName === 'microsoftAzure') {
return { title: 'Microsoft Azure OpenAI', }
}
@@ -112,17 +112,17 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
if (providerName === 'anthropic') return 'Get your [API Key here](https://console.anthropic.com/settings/keys).'
if (providerName === 'openAI') return 'Get your [API Key here](https://platform.openai.com/api-keys).'
if (providerName === 'deepseek') return 'Get your [API Key here](https://platform.deepseek.com/api_keys).'
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys).'
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey).'
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys). Read about [rate limits here](https://openrouter.ai/docs/api-reference/limits).'
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey). Read about [rate limits here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits).'
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).`
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
if (providerName === 'ollama') return '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).'
if (providerName === 'vLLM') return '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).'
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
@@ -149,9 +149,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'openAICompatible' ? 'sk-key...' :
providerName === 'xAI' ? 'xai-key...' :
providerName === 'mistral' ? 'api-key...' :
// providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
isPasswordField: true,
}
@@ -162,10 +162,10 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'vLLM' ? 'Endpoint' :
providerName === 'lmStudio' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
// providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
@@ -177,14 +177,17 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
}
// else if (settingName === 'region') {
// // vertex only
// return {
// title: 'Region',
// placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
// : ''
// }
// }
else if (settingName === 'headersJSON') {
return { title: 'Custom Headers', placeholder: '{ "X-Request-Id": "..." }' }
}
else if (settingName === 'region') {
// vertex only
return {
title: 'Region',
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
: ''
}
}
else if (settingName === 'azureApiVersion') {
// azure only
return {
@@ -196,11 +199,11 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
else if (settingName === 'project') {
return {
title: providerName === 'microsoftAzure' ? 'Resource'
// : providerName === 'googleVertex' ? 'Project'
: '',
: providerName === 'googleVertex' ? 'Project'
: '',
placeholder: providerName === 'microsoftAzure' ? 'my-resource'
// : providerName === 'googleVertex' ? 'my-project'
: ''
: providerName === 'googleVertex' ? 'my-project'
: ''
}
@@ -228,9 +231,10 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
// region: undefined, // googleVertex
region: undefined, // googleVertex
project: undefined,
azureApiVersion: undefined,
headersJSON: undefined,
}
@@ -324,12 +328,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
_didFillInProviderSettings: undefined,
},
// googleVertex: { // aggregator (serves models from multiple providers)
// ...defaultCustomSettings,
// ...defaultProviderSettings.googleVertex,
// ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
// _didFillInProviderSettings: undefined,
// },
googleVertex: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.googleVertex,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
_didFillInProviderSettings: undefined,
},
microsoftAzure: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.microsoftAzure,
@@ -430,6 +434,7 @@ export type GlobalSettings = {
showInlineSuggestions: boolean;
includeToolLintErrors: boolean;
isOnboardingComplete: boolean;
customRootCertificates: string[]; // Paths to custom root certificates
}
export const defaultGlobalSettings: GlobalSettings = {
@@ -443,6 +448,7 @@ export const defaultGlobalSettings: GlobalSettings = {
showInlineSuggestions: true,
includeToolLintErrors: true,
isOnboardingComplete: false,
customRootCertificates: [],
}
export type GlobalSettingName = keyof GlobalSettings
@@ -462,13 +468,28 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
export type ModelSelectionOptions = {
reasoningEnabled?: boolean;
reasoningBudget?: number;
reasoningEffort?: string;
}
export type OptionsOfModelSelection = {
[featureName in FeatureName]: Partial<{
[providerName in ProviderName]: {
[modelName: string]:
ModelSelectionOptions | undefined
[modelName: string]: ModelSelectionOptions | undefined
}
}>
}
export type OverridesOfModel = {
[providerName in ProviderName]: {
[modelName: string]: Partial<ModelOverrides> | undefined
}
}
const overridesOfModel = {} as OverridesOfModel
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
export const defaultOverridesOfModel = overridesOfModel
@@ -23,6 +23,9 @@ export const extractReasoningWrapper = (
let fullTextSoFar = ''
let fullReasoningSoFar = ''
if (!thinkTags[0] || !thinkTags[1]) throw new Error(`thinkTags must not be empty if provided. Got ${JSON.stringify(thinkTags)}.`)
let onText_ = onText
onText = (params) => {
onText_(params)
@@ -10,14 +10,24 @@ import { Ollama } from 'ollama';
import OpenAI, { ClientOptions } from 'openai';
import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
// import { GoogleAuth } from 'google-auth-library'
import { Tool as GeminiTool, FunctionDeclaration, GoogleGenAI, ThinkingConfig, Schema, Type } from '@google/genai';
import { GoogleAuth } from 'google-auth-library'
/* eslint-enable */
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
const getGoogleApiKey = async () => {
// modulelevel singleton
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
const key = await auth.getAccessToken()
if (!key) throw new Error(`Google API failed to generate a key.`)
return key
}
@@ -29,11 +39,16 @@ type InternalCommonMessageParams = {
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
modelName: string;
_setAborter: (aborter: () => void) => void;
}
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
type SendChatParams_Internal = InternalCommonMessageParams & {
messages: LLMChatMessage[];
separateSystemMessage: string | undefined;
chatMode: ChatMode | null;
}
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
@@ -42,15 +57,17 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
// const getGoogleApiKey = async () => {
// // modulelevel singleton
// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
// const key = await auth.getAccessToken()
// if (!key) throw new Error(`Google API failed to generate a key.`)
// return key
// }
const parseHeadersJSON = (s: string | undefined): Record<string, string | null | undefined> | undefined => {
if (!s) return undefined
try {
return JSON.parse(s)
} catch (e) {
throw new Error(`Error parsing OpenAI-Compatible headers: ${s} is not a valid JSON.`)
}
}
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
const commonPayloadOpts: ClientOptions = {
dangerouslyAllowBrowser: true,
@@ -88,20 +105,17 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
...commonPayloadOpts,
})
}
else if (providerName === 'gemini') {
else if (providerName === 'googleVertex') {
// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
const apiKey = await getGoogleApiKey()
return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
}
// else if (providerName === 'googleVertex') {
// // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
// const thisConfig = settingsOfProvider[providerName]
// const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
// return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
// }
else if (providerName === 'microsoftAzure') {
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
const thisConfig = settingsOfProvider[providerName]
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions?api-version=${thisConfig.azureApiVersion}`
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
}
@@ -111,7 +125,8 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
const headers = parseHeadersJSON(thisConfig.headersJSON)
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, defaultHeaders: headers, ...commonPayloadOpts })
}
else if (providerName === 'groq') {
const thisConfig = settingsOfProvider[providerName]
@@ -130,8 +145,14 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
}
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, overridesOfModel }: SendFIMParams_Internal) => {
const {
modelName,
supportsFIM,
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
@@ -140,7 +161,7 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens
return
}
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload: additionalOpenAIPayload })
openai.completions
.create({
model: modelName,
@@ -193,7 +214,7 @@ const openAITools = (chatMode: ChatMode) => {
return openAITools
}
const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
if (!isAToolName(name)) return null
const rawParams: RawToolParamsObj = {}
let input: unknown
@@ -215,19 +236,24 @@ const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: str
// ------------ OPENAI-COMPATIBLE ------------
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_)
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
const { canIOReasoning, openSourceThinkTags } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
const includeInPayload = {
...providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo),
...additionalOpenAIPayload
}
// tools
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
@@ -242,6 +268,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
messages: messages as any,
stream: true,
...nativeToolsObj,
...additionalOpenAIPayload
// max_completion_tokens: maxTokens,
}
@@ -297,6 +324,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
fullReasoningSoFar += newReasoning
}
// call onText
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
@@ -309,7 +337,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
@@ -399,21 +427,21 @@ const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBloc
}
// ------------ ANTHROPIC ------------
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
} = getModelCapabilities(providerName, modelName_)
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
const thisConfig = settingsOfProvider.anthropic
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// anthropic-specific - max tokens
const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled })
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
// tools
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
@@ -438,7 +466,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
})
// manually parse out tool results
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
@@ -523,8 +551,8 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
// ------------ MISTRAL ------------
// https://docs.mistral.ai/api/#tag/fim
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, overridesOfModel, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_, overridesOfModel)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
@@ -544,6 +572,7 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
stop: messages.stopTokens,
})
.then(async response => {
// unfortunately, _setAborter() does not exist
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
const fullText = typeof content === 'string' ? content
@@ -620,6 +649,169 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
})
}
// ---------------- GEMINI NATIVE IMPLEMENTATION ----------------
const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
return {
name,
description,
parameters: {
type: Type.OBJECT,
properties: Object.entries(params).reduce((acc, [key, value]) => {
acc[key] = {
type: Type.STRING,
description: value.description
};
return acc;
}, {} as Record<string, Schema>)
}
} satisfies FunctionDeclaration
}
const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => {
const allowedTools = availableTools(chatMode)
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
const functionDecls: FunctionDeclaration[] = []
for (const t in allowedTools ?? {}) {
functionDecls.push(toGeminiFunctionDecl(allowedTools[t]))
}
const tools: GeminiTool = { functionDeclarations: functionDecls, }
return [tools]
}
// Implementation for Gemini using Google's native API
const sendGeminiChat = async ({
messages,
separateSystemMessage,
onText,
onFinalMessage,
onError,
settingsOfProvider,
overridesOfModel,
modelName: modelName_,
_setAborter,
providerName,
modelSelectionOptions,
chatMode,
}: SendChatParams_Internal) => {
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
const thisConfig = settingsOfProvider[providerName]
const {
modelName,
specialToolFormat,
// reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
// const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
// const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
// const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
const thinkingConfig: ThinkingConfig | undefined = !reasoningInfo?.isReasoningEnabled ? undefined
: reasoningInfo.type === 'budget_slider_value' ?
{ thinkingBudget: reasoningInfo.reasoningBudget }
: undefined
// tools
const potentialTools = chatMode !== null ? geminiTools(chatMode) : undefined
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
potentialTools
: undefined
// instance
const genAI = new GoogleGenAI({ apiKey: thisConfig.apiKey });
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
// when receive text
let fullReasoningSoFar = ''
let fullTextSoFar = ''
let toolName = ''
let toolParamsStr = ''
let toolId = ''
genAI.models.generateContentStream({
model: modelName,
config: {
systemInstruction: separateSystemMessage,
thinkingConfig: thinkingConfig,
tools: toolConfig,
},
contents: messages as GeminiLLMChatMessage[],
})
.then(async (stream) => {
_setAborter(() => { stream.return(fullTextSoFar); });
// Process the stream
for await (const chunk of stream) {
// message
const newText = chunk.text ?? ''
fullTextSoFar += newText
// tool call
const functionCalls = chunk.functionCalls
if (functionCalls && functionCalls.length > 0) {
const functionCall = functionCalls[0] // Get the first function call
toolName = functionCall.name ?? ''
toolParamsStr = JSON.stringify(functionCall.args ?? {})
toolId = functionCall.id ?? ''
}
// (do not handle reasoning yet)
// call onText
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
})
}
// on final
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
} else {
if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
})
.catch(error => {
const message = error?.message
if (typeof message === 'string') {
if (error.message?.includes('API key')) {
onError({ message: invalidApiKeyMessage(providerName), fullError: error });
}
else if (error?.message?.includes('429')) {
onError({ message: 'Rate limit reached. ' + error, fullError: error });
}
else
onError({ message: error + '', fullError: error });
}
else {
onError({ message: error + '', fullError: error });
}
})
};
type CallFnOfProvider = {
@@ -647,7 +839,7 @@ export const sendLLMMessageToProviderImplementation = {
list: null,
},
gemini: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendChat: (params) => sendGeminiChat(params),
sendFIM: null,
list: null,
},
@@ -688,20 +880,21 @@ export const sendLLMMessageToProviderImplementation = {
},
lmStudio: {
// lmStudio has no suffix parameter in /completions, so sendFIM might not work
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null, // lmStudio has no suffix parameter in /completions
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
list: (params) => _openaiCompatibleList(params),
},
liteLLM: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
list: null,
},
googleVertex: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
// googleVertex: {
// sendChat: (params) => _sendOpenAICompatibleChat(params),
// sendFIM: null,
// list: null,
// },
microsoftAzure: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
@@ -20,6 +20,7 @@ export const sendLLMMessage = async ({
settingsOfProvider,
modelSelection,
modelSelectionOptions,
overridesOfModel,
chatMode,
separateSystemMessage,
}: SendLLMMessageParams,
@@ -33,13 +34,6 @@ export const sendLLMMessage = async ({
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureLLMEvent = (eventId: string, extras?: object) => {
let totalTokens = 0
if (messagesType === 'chatMessages') {
for (const m of messages_) totalTokens += m.content.length
}
else {
totalTokens = messages_.prefix.length + messages_.suffix.length
}
metricsService.capture(eventId, {
providerName,
@@ -48,13 +42,10 @@ export const sendLLMMessage = async ({
numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length,
...messagesType === 'chatMessages' ? {
numMessages: messages_?.length,
messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
} : messagesType === 'FIMMessage' ? {
prefixLength: messages_.prefix.length,
suffixLength: messages_.suffix.length,
} : {},
totalTokens,
...loggingExtras,
...extras,
})
@@ -74,9 +65,9 @@ export const sendLLMMessage = async ({
}
const onFinalMessage: OnFinalMessage = (params) => {
const { fullText, fullReasoning } = params
const { fullText, fullReasoning, toolCall } = params
if (_didAbort) return
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds(), toolCallName: toolCall?.name })
onFinalMessage_(params)
}
@@ -103,7 +94,7 @@ export const sendLLMMessage = async ({
if (messagesType === 'chatMessages')
captureLLMEvent(`${loggingName} - Sending Message`, { userMessageLength: messages_?.[messages_.length - 1]?.content.length })
captureLLMEvent(`${loggingName} - Sending Message`, {})
else if (messagesType === 'FIMMessage')
captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length })
@@ -116,15 +107,15 @@ export const sendLLMMessage = async ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage })
return
}
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
onError({ message: `Error running Autocomplete with ${providerName} - ${modelName}.`, fullError: null })
return
}
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
@@ -94,7 +94,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
const data = await response.json();
const version = data.tag_name;
const myVersion = `${this._productService.version}.${this._productService.release}`
const myVersion = this._productService.version
const latestVersion = version
const isUpToDate = myVersion === latestVersion // only makes sense if response.ok