Compare commits

...

519 Commits

Author SHA1 Message Date
Andrew Pareles 64760aae13 comment 2024-12-31 13:54:08 -08:00
Andrew Pareles d0d1124543 delete old files 2024-12-31 13:47:41 -08:00
Andrew Pareles 90a3fd0a21 merge 2024-12-31 13:45:48 -08:00
Andrew Pareles 11ed2545c7 Merge remote-tracking branch 'origin/main' into new-services 2024-12-31 13:40:20 -08:00
Andrew Pareles 3d1df92f6a re-add deletion logic 2024-12-31 13:39:31 -08:00
Andrew Pareles 4ca16caa4e fix misc history issues with ctrlk - looks good!!! 2024-12-31 02:24:34 -08:00
Andrew Pareles 06b5cca59a history snapshot is only for this URI 2024-12-30 23:29:05 -08:00
Andrew Pareles e3c292eee7 progress towards ctrlK 2024-12-30 22:14:55 -08:00
Andrew Pareles 99c3786d06 diffarea resize logic 2024-12-30 19:37:52 -08:00
Andrew Pareles 6e40a92315 add FIM streaming + instructions 2024-12-30 15:20:55 -08:00
Andrew Pareles a9b2ff631d ctrlK progress... 2024-12-30 03:04:32 -08:00
Andrew Pareles ef8bba00b9 update ctrlk 2024-12-29 19:16:16 -08:00
Andrew Pareles ff7830e704 resize observer mostly works 2024-12-28 22:30:46 -05:00
Andrew Pareles 01a345a391 ctrlk double redundancy fix 2024-12-28 21:28:20 -05:00
Andrew Pareles 33909b3143 add ctrl+K 2024-12-28 20:03:47 -05:00
Andrew Pareles 9a9d0da91e remove errors 2024-12-27 23:56:24 -05:00
Andrew Pareles 1df044fb02 Update product.json 2024-12-25 22:23:01 -05:00
Andrew Pareles dfbd729fa4 nightly progress 2024-12-24 23:45:58 -05:00
Andrew Pareles ef9d0daef4 minor chat styles 2024-12-23 15:37:06 -08:00
Andrew Pareles ffe5a41b01 fix rounding 2024-12-23 04:33:49 -08:00
Andrew Pareles 372beb7a2d rm temp 2024-12-23 03:30:05 -08:00
Andrew Pareles 406f29bb5a + 2024-12-23 03:29:49 -08:00
Andrew Pareles ee8b98ff74 minor changes 2024-12-23 03:00:19 -08:00
Andrew Pareles ffb0549c7c update zoneStyleService 2024-12-23 03:00:19 -08:00
Andrew Pareles 8f9c2f0630 misc 2024-12-23 03:00:19 -08:00
Andrew Pareles 84bfc26dce debugging 2024-12-23 03:00:10 -08:00
Andrew Pareles 80a87ff3c6 improvements but might still be buggy 2024-12-23 02:43:19 -08:00
mp 806ec939e5 ux for providers 2024-12-23 02:34:23 -08:00
Andrew Pareles 98a22d2eb6 minor changes 2024-12-23 00:16:37 -08:00
Andrew Pareles ffa31bc289 update zoneStyleService 2024-12-23 00:14:54 -08:00
Andrew Pareles a1ab8a5796 misc 2024-12-22 23:52:21 -08:00
mp 70c7d0261c correct bg 2024-12-22 21:41:37 -08:00
mp e7af8d9448 selection style 2024-12-22 21:28:09 -08:00
mp e1feaba74a warning icon 2024-12-22 02:12:24 -08:00
mp 8456f979ba warning icon 2024-12-22 02:07:29 -08:00
Andrew Pareles 6630d596f6 code box UI 2024-12-21 22:43:34 -08:00
Andrew Pareles a435a59a2e inlineDiffEdits add support for ctrl+K 2024-12-21 22:40:08 -08:00
mp 9cf4980004 selection 2024-12-21 22:37:08 -08:00
Mathew Pareles 6298be394d indentation 2024-12-21 21:13:21 -08:00
Andrew Pareles e647ce0054 fix + mount sidebar on init 2024-12-21 18:46:45 -08:00
Andrew Pareles 5fb30fed87 sidebar action 2024-12-21 18:29:15 -08:00
mp 7cc57bf086 input styles 2024-12-21 18:19:19 -08:00
Andrew Pareles 3fb6a4890d watermark 2024-12-21 17:51:15 -08:00
mp 97fd332c4e add code editor block 2024-12-21 17:29:02 -08:00
Andrew Pareles 81282e16b3 minor contrib update 2024-12-21 00:15:14 -08:00
Andrew Pareles 382275fa9f delete built-in extensions so the build works 2024-12-21 00:00:57 -08:00
Andrew Pareles 91f8321278 delete autoenable 2024-12-20 02:08:30 -08:00
Andrew Pareles 9b1f21d7c1 Merge pull request #185 from voideditor/model-selection
Daily UI improvements
2024-12-20 02:07:49 -08:00
Andrew Pareles 739ac2562b protected fix 2024-12-19 22:44:29 -08:00
Andrew Pareles 298423928e update styles and descs 2024-12-19 18:44:26 -08:00
mp 702d11ed3e Merge branch 'polishing-changes' into model-selection 2024-12-19 17:09:01 -08:00
Andrew Pareles 4513b732d6 all changes 2024-12-19 17:07:58 -08:00
Andrew Pareles 78e72f0c80 300 2024-12-19 17:00:16 -08:00
Andrew Pareles f457f006fb settings page styles 2024-12-19 04:05:31 -08:00
mp d6074e7ab5 ctrlk draft 2024-12-19 02:14:26 -08:00
Andrew Pareles 3669401819 upgrade to switches 2024-12-19 01:28:43 -08:00
Andrew Pareles ee7d0ed836 Merge pull request #181 from bjoaquinc/react-build-watcher
Fix React build crashing watch-client and added watch mode
2024-12-18 19:45:24 -08:00
Andrew Pareles ba87b4f0de update contrib 2024-12-18 19:40:27 -08:00
Andrew Pareles e4bc21ffb1 Merge pull request #182 from voideditor/model-selection
UI Improvements and Model Selection
2024-12-18 19:33:57 -08:00
Andrew Pareles 81b78b7765 auto fetch model works 2024-12-18 19:31:50 -08:00
Andrew Pareles c3c53b5e81 slightly fix refresh state 2024-12-18 16:48:04 -08:00
Andrew Pareles f49a6f8e41 minor style changes and refresh model works in general 2024-12-18 16:06:53 -08:00
mp 5a26aa8746 Merge branch 'model-selection' into polishing-changes 2024-12-18 15:45:18 -08:00
Andrew Pareles 8661fe1417 fix selection styles and add void watermark (buttons look bad) 2024-12-18 13:35:34 -08:00
Joaquin Coromina 2a519157da Merge remote-tracking branch 'origin/react-build-watcher' into recovered-code
Added a watcher that syncs react with watch-client without crashing#
2024-12-18 18:47:37 +08:00
mp ac8365def4 ctrl+L 2024-12-18 02:29:11 -08:00
Joaquin Coromina 610b52587b Added watchreact npm command, resolved watch-client crashes during updates, and included nodemon as a dependency 2024-12-18 18:24:17 +08:00
mp 01efab6a1d cmd+L prompt 2024-12-17 19:30:14 -08:00
Andrew Pareles 7f8e1ff00d Merge pull request #180 from voideditor/model-selection
Misc UI Improvements
2024-12-17 18:16:07 -08:00
Andrew Pareles 5b55eed08a threads styles + misc 2024-12-17 18:15:09 -08:00
Andrew Pareles d618223289 misc improvements. need to add max h on thread histroy 2024-12-17 01:55:32 -08:00
Andrew Pareles 7752278720 settings styles 2024-12-17 01:33:33 -08:00
Andrew Pareles 597c04f63b settings UI 2024-12-16 23:08:01 -08:00
mp a7c4a1b179 input box style 2024-12-16 23:02:33 -08:00
Andrew Pareles a7aab9cf86 some UI for models! 2024-12-16 21:49:03 -08:00
Andrew Pareles c44497510b add responsive dark mode 2024-12-16 18:57:28 -08:00
Andrew Pareles c485d2c478 react updates 2024-12-16 17:43:07 -08:00
Andrew Pareles dfdf29fb36 rename 2024-12-16 15:05:49 -08:00
Andrew Pareles 420fb1a186 icon is incredibly annoying 2024-12-16 01:21:55 -08:00
Andrew Pareles 78a48bae13 add model selection 2024-12-16 00:17:50 -08:00
mp fc187e0028 sidebar scrolllock 2024-12-15 23:06:25 -08:00
mp f973f8d4e7 fix selection range bug 2024-12-15 22:24:16 -08:00
Andrew Pareles 8403eed8ca fix hook 2024-12-15 21:42:31 -08:00
Andrew Pareles 718b8cc61b add void settings in lower left 2024-12-15 21:10:17 -08:00
mp f6924df09b Fix rerenders 2024-12-15 20:52:43 -08:00
Andrew Pareles 8b25f80e9b model selection is accurate now, and lots of files moved to be more accurate 2024-12-15 20:43:11 -08:00
mp a42ff2d59a sidebar + ux 2024-12-15 01:33:02 -08:00
Andrew Pareles 0589a2a5ec remove maxtokens 2024-12-15 01:11:07 -08:00
Andrew Pareles 7dda4bdc08 add editor pane 2024-12-15 00:30:21 -08:00
Andrew Pareles f21ef61677 .js fix 2024-12-14 21:19:49 -08:00
Andrew Pareles 56b6d406d6 Merge remote-tracking branch 'origin/main' into model-selection 2024-12-14 21:16:43 -08:00
Andrew Pareles 2ae29a50e5 fix 2024-12-14 21:16:16 -08:00
mp 9673e974ea rename + merge 2024-12-14 20:47:27 -08:00
mp 842d388669 multiple selections 2024-12-14 20:39:50 -08:00
Andrew Pareles f146385e1d Update CONTRIBUTING.md 2024-12-14 20:17:30 -08:00
Andrew Pareles bb87726b34 Update CONTRIBUTING.md 2024-12-14 20:15:15 -08:00
Andrew Pareles deb30b49bc Update CONTRIBUTING.md 2024-12-14 19:51:28 -08:00
Andrew Pareles 56923eec52 Update CONTRIBUTING.md 2024-12-14 19:50:16 -08:00
Andrew Pareles d2692d2594 Update CONTRIBUTING.md 2024-12-14 19:47:44 -08:00
Andrew Pareles 5d44fdc17d Update CONTRIBUTING.md 2024-12-14 19:47:21 -08:00
Andrew Pareles aada60c5be Update CONTRIBUTING.md 2024-12-14 19:45:45 -08:00
Andrew Pareles b53e5d4886 Update CONTRIBUTING.md 2024-12-14 19:45:37 -08:00
Andrew Pareles f299998994 Update CONTRIBUTING.md 2024-12-14 19:41:18 -08:00
Andrew Pareles 967292e615 Update CONTRIBUTING.md 2024-12-14 19:31:37 -08:00
Andrew Pareles 19e3ebefda misc 2024-12-14 18:07:56 -08:00
Andrew Pareles 7cf6c9d798 Merge pull request #175 from voideditor/model-selection
Minor Contrib.md changes
2024-12-14 18:04:37 -08:00
Andrew Pareles 14e314e5b8 update contrib 2024-12-14 18:03:50 -08:00
Andrew Pareles 4e6e0b3bde Merge pull request #172 from voideditor/model-selection
Model selection
2024-12-14 17:58:38 -08:00
Andrew Pareles 63b086b786 dummy commit 2024-12-14 17:55:44 -08:00
Andrew Pareles bda6ad06c9 Merge branch 'main' into model-selection 2024-12-14 17:53:38 -08:00
Andrew Pareles 829d82850d rename Secondary Side Bar to Void Side Bar 2024-12-14 17:51:21 -08:00
Andrew Pareles 3991372f5b misc improvements 2024-12-14 17:51:07 -08:00
Andrew Pareles 9b427c78bf fix react state sync bug 2024-12-13 01:28:55 -08:00
Andrew Pareles 5dad4c8f06 relevant ollama update vals 2024-12-13 01:05:33 -08:00
Andrew Pareles e2d39fe4b1 ollama refreshes! 2024-12-13 00:41:34 -08:00
Andrew Pareles 1365400f21 llmMessageService now multiplexes between sendLLMMessage, ollama, etc 2024-12-12 19:30:45 -08:00
Andrew Pareles 94f4d2caaf add ollamaList to llmMessageService 2024-12-12 19:30:45 -08:00
Andrew Pareles 696abc39fd add ollama model fetcher, but proxy ignores its functions 2024-12-12 19:30:45 -08:00
mp abc7151cd6 Merge branch 'model-seln-2' into model-selection 2024-12-12 16:50:37 -08:00
Andrew Pareles c86f47743e update links 2024-12-12 16:42:48 -08:00
Andrew Pareles 9d6eb62c3e revert accidental change 2024-12-12 16:31:21 -08:00
Andrew Pareles 2f26b34a96 remove connect-src * that was introduced in testing 2024-12-12 16:25:23 -08:00
Andrew Pareles 5807c94a61 tailwind 2024-12-12 14:49:36 -08:00
Andrew Pareles df23314bd2 add css to useful links 2024-12-12 14:37:54 -08:00
Andrew Pareles 6703aa1545 get ready to sync ollama models 2024-12-12 01:42:06 -08:00
mp aaab289ece native tailwind colors 2024-12-12 01:10:52 -08:00
Andrew Pareles d809839530 add dummy models 2024-12-11 21:30:13 -08:00
Andrew Pareles 9e583960ae visual improvements 2024-12-11 21:17:24 -08:00
Andrew Pareles 19737d4cdc minor display change 2024-12-11 20:06:16 -08:00
mp 901579a445 Merge remote-tracking branch 'origin/model-selection' into model-seln-2 2024-12-11 20:04:37 -08:00
mp 051f8f38e7 Sidebar restructure 2024-12-11 20:02:07 -08:00
Andrew Pareles 07fcb3a72c better model seln UI + error handling works much better! 2024-12-11 19:53:36 -08:00
Andrew Pareles 62cdaba44b npm run react 2024-12-11 02:57:17 -08:00
Andrew Pareles 3147a92463 Update VOID_USEFUL_LINKS.md 2024-12-11 02:53:59 -08:00
Andrew Pareles d175ac20ef try dropdownmenu; didn't work 2024-12-11 02:40:19 -08:00
Andrew Pareles 42a2dec781 minor welcome changes 2024-12-11 02:19:22 -08:00
Andrew Pareles dbef72c01c update scope-tailwind to support -pr-5 2024-12-11 02:04:33 -08:00
Andrew Pareles 6ddf977c5d select box style 2024-12-11 01:47:57 -08:00
Andrew Pareles 522f136872 license comment 2024-12-11 01:28:55 -08:00
Andrew Pareles a28194c601 add sync state for dropdown 2024-12-11 01:24:04 -08:00
Andrew Pareles cb2de5505b fix 2024-12-11 00:49:52 -08:00
Andrew Pareles f84c964d10 finished model selection 2024-12-11 00:48:48 -08:00
Andrew Pareles bb4f99f687 error state, sendLLMMessage fix, continue adding state for provider/model pair selection 2024-12-10 23:29:39 -08:00
Andrew Pareles 49b43820b2 state - pick a provider/model 2024-12-10 23:24:36 -08:00
Andrew Pareles b22bbf9639 add not recommended line 2024-12-10 19:03:42 -08:00
Andrew Pareles a1ff458007 add deps and remove devDeps 2024-12-10 19:02:17 -08:00
Andrew Pareles fb263f9d81 update contrib for bundling 2024-12-10 17:54:58 -08:00
Andrew Pareles 72deba43dd consistent error messages 2024-12-10 17:41:13 -08:00
mp 24fdb1ca0d minor changes 2024-12-10 17:29:55 -08:00
Andrew Pareles f20f41c804 minor 'true' update 2024-12-10 02:42:32 -08:00
Andrew Pareles 66037e2049 update init state properly 2024-12-10 02:39:30 -08:00
Andrew Pareles adb504f2e0 reactify Widget 2024-12-10 02:09:06 -08:00
Andrew Pareles 8b90d7827d un-delete void channels 2024-12-10 00:25:06 -08:00
mp 62e1657274 change config key + merge 2024-12-09 19:45:49 -08:00
Andrew Pareles 7f776eebdb minor key 2024-12-09 19:45:37 -08:00
Andrew Pareles 0db0513752 finish merge errors 2024-12-09 19:25:54 -08:00
Andrew Pareles d8b6b3c306 Merge branch 're-add-autocomplete' into model-seln-2 2024-12-09 19:12:57 -08:00
Andrew Pareles f7c9d732f5 update type info 2024-12-09 19:01:08 -08:00
Andrew Pareles 8a95b13711 minor changes 2024-12-09 17:03:29 -08:00
Andrew Pareles 55884ed1c9 checkbox 2024-12-09 16:45:03 -08:00
Andrew Pareles 582b25dc44 build works 2024-12-09 16:26:31 -08:00
mp 252af4c863 build script 2024-12-09 16:17:53 -08:00
Andrew Pareles e5ac36facd add new config type for each feature, UI dump 2024-12-09 02:41:27 -08:00
Andrew Pareles 77fc2d2d4a temp 2024-12-08 18:44:04 -08:00
Andrew Pareles 298adac06e fix input box 2024-12-08 17:57:48 -08:00
Andrew Pareles 0f920fb867 remove void Widget inputs for now 2024-12-07 17:20:02 -08:00
mp 02112fbc4f allow selection by feature + provider (incomplete) 2024-12-07 00:28:13 -08:00
mp 05241649ff comment grep + _uiConfig change 2024-12-06 18:16:18 -08:00
Andrew Pareles 4ba00f6216 scope-tailwind doc 2024-12-06 02:55:28 -08:00
Andrew Pareles 296cd1b477 Update CONTRIBUTING.md 2024-12-06 02:51:30 -08:00
Andrew Pareles 2148fe77ec forgot to remove these 2024-12-06 01:33:10 -08:00
Andrew Pareles 1f692ff01a update contributing 2024-12-06 01:22:47 -08:00
Andrew Pareles 8e4f943e30 move node to electron-main, leaving react stuff in contrib/ 2024-12-06 01:03:43 -08:00
Andrew Pareles a52eba91c9 react updates 2024-12-05 22:57:55 -08:00
mp 695d6a1e30 Disable autocomplete 2024-12-05 19:52:05 -08:00
Andrew Pareles a647530d77 add native input selection in settings 2024-12-05 01:17:05 -08:00
Andrew Pareles 18e3f19512 inputbox ref is now an internal widget (and works better!) 2024-12-04 23:49:45 -08:00
Andrew Pareles 86294c676a fix copy 2024-12-04 23:48:37 -08:00
Andrew Pareles 7cfd9e5580 better msg display 2024-12-04 22:07:25 -08:00
Andrew Pareles d29eca457a fix 0 2024-12-04 21:43:03 -08:00
Andrew Pareles 0855376e02 update message display 2024-12-04 21:36:33 -08:00
Andrew Pareles e1692758c6 ui improvements - abort fix 2024-12-04 20:47:41 -08:00
Andrew Pareles 411167ee44 Merge remote-tracking branch 'origin/main' into ui-improvements 2024-12-04 19:02:47 -08:00
Andrew Pareles 389efb4701 fix scroll 2024-12-04 18:54:05 -08:00
mp 48f9ac2ba1 Minor changes 2024-12-04 18:50:09 -08:00
mp f9198d4d97 Remove accepted autocomplete characters 2024-12-04 17:50:24 -08:00
Andrew Pareles eafc3dc0c9 Merge pull request #144 from pythons/feature/llm_provider
refactor: modularize LLM providers and types
2024-12-04 15:12:59 -08:00
Andrew Pareles f2b176dfda remove extensions/ code 2024-12-04 15:12:45 -08:00
Andrew Pareles 02781927e1 Merge branch 'main' into pr/pythons/144 2024-12-04 15:11:42 -08:00
Andrew Pareles 4b3f881e1f rm comment 2024-12-04 15:08:40 -08:00
Andrew Pareles f3653c1c48 clarify config access 2024-12-04 15:07:11 -08:00
Andrew Pareles 3a1955901e move natively 2024-12-04 14:46:36 -08:00
Andrew Pareles e3ce68d2d6 Merge remote-tracking branch 'origin/main' into pr/pythons/144 2024-12-04 14:26:23 -08:00
Andrew Pareles ab3d44387e Merge pull request #141 from Piyu-Pika/Gemini
Add Groq
2024-12-04 14:20:38 -08:00
Andrew Pareles 96e24a89e6 re-remove extension package.json 2024-12-04 14:19:42 -08:00
Andrew Pareles 25cc6aeb3b add readme 2024-12-04 14:18:51 -08:00
Andrew Pareles 4cac92689a integrate into codebase 2024-12-04 14:16:34 -08:00
Piyush Bhardwaj 7444488278 Merge branch 'main' into pr/Piyu-Pika/141 2024-11-27 16:03:59 +05:30
mp a6cf723dbe Fix space issue 2024-11-26 19:01:38 -08:00
Andrew Pareles 7e22b80986 Merge pull request #161 from voideditor/docs-improvements
Improve documentation
2024-11-26 16:39:46 -08:00
Andrew Pareles 9ec4f3da01 void useful links update 2024-11-26 16:39:01 -08:00
mp ef9ac7a9e3 Merge branch 'main' into re-add-autocomplete 2024-11-26 16:30:38 -08:00
mp 48426403f3 Re-added autocomplete 2024-11-26 15:15:05 -08:00
Andrew Pareles 86c9a17f8d improve message 2024-11-26 02:59:16 -08:00
Andrew Pareles d21f2c4d62 fix file capitalization part 2 2024-11-26 02:52:20 -08:00
Andrew Pareles 733256feab fix file capitalization part 1 2024-11-26 02:51:56 -08:00
Andrew Pareles f9ce9de316 Merge pull request #159 from voideditor/fix-cors
Fix CORS + CSP
2024-11-26 02:22:57 -08:00
Andrew Pareles 9f27b17f11 ignore e.stack 2024-11-26 02:22:05 -08:00
Andrew Pareles 8ff4f1c6e1 update error handling 2024-11-26 02:16:57 -08:00
Andrew Pareles f6f6259f23 fix error handling 2024-11-26 01:55:42 -08:00
Andrew Pareles c7e9175fd7 add ability to abort 2024-11-26 01:08:25 -08:00
Andrew Pareles bdc3c9cb7e proxy works! 2024-11-26 00:41:31 -08:00
Andrew Pareles 790a2b6a4f llm messages work! sent from node! 2024-11-26 00:25:47 -08:00
Andrew Pareles e6c779d91a progress 2024-11-24 23:43:10 -08:00
Andrew Pareles aa95ceafd2 proxy to node appears to work!! 2024-11-24 20:39:15 -08:00
Andrew Pareles 9ef1aa2c60 fix cors progress 2024-11-24 20:28:54 -08:00
Andrew Pareles e4107e1257 add server? 2024-11-24 17:53:57 -08:00
Andrew Pareles 1d5fef43c1 improved error handling 2024-11-23 16:05:26 -08:00
Andrew Pareles 51280d4e77 error ui improvements 2024-11-22 00:43:07 -08:00
Andrew Pareles d5ccc6f777 minor changes 2024-11-21 22:37:31 -08:00
mp 4ea1bfa9c3 add structure for re-adding autocomplete 2024-11-21 19:05:54 -08:00
Andrew Pareles c58af2a3af Merge pull request #139 from voideditor/actual-editor-insets
Native Editor Insets
2024-11-21 18:32:25 -08:00
Andrew Pareles d857d54847 reference extension 2024-11-21 18:29:08 -08:00
Andrew Pareles a7cbed395b Merge branch 'autocomplete' into actual-editor-insets 2024-11-21 18:12:28 -08:00
Andrew Pareles f6479a0668 Merge remote-tracking branch 'origin/main' into actual-editor-insets 2024-11-21 17:56:19 -08:00
Andrew Pareles a50b5c6407 add minor debugging 2024-11-21 17:54:43 -08:00
mp 53d19d819c autocomplete UX 2024-11-21 05:58:43 -08:00
Piyush Bhardwaj a3e337fe2d Merge branch 'main' into Gemini 2024-11-21 17:51:24 +05:30
Andrew Pareles 1bb243bc34 comment fixes build 2024-11-21 03:57:17 -08:00
Andrew Pareles faaf3a738a rm console.logs 2024-11-21 03:56:46 -08:00
Andrew Pareles e8bbe4bf5a tsup works 2024-11-21 03:56:09 -08:00
Andrew Pareles 469bba178a Merge pull request #155 from jcommaret/actual-editor-insets
Build.js : Fixing tsup command
2024-11-21 03:51:32 -08:00
Jérôme Commaret 879595dbe6 Build.js : Fixing tsup command 2024-11-21 12:47:44 +01:00
Andrew Pareles f7d2e95d37 update contrib 2024-11-20 23:01:29 -08:00
Andrew Pareles 9dbc7f5caa + 2024-11-20 22:43:30 -08:00
Andrew Pareles 1cc35b1d79 fix contrib* 2024-11-20 22:42:03 -08:00
Andrew Pareles 2473f210bc update contrib 2024-11-20 22:39:16 -08:00
Andrew Pareles 604578bb19 update contrib 2024-11-20 22:32:58 -08:00
Andrew Pareles 1d73aad2ed revert FIM testing code 2024-11-20 22:29:18 -08:00
Andrew Pareles 8508c2dd8e Merge branch 'main' into actual-editor-insets 2024-11-20 21:59:34 -08:00
Andrew Pareles a8c5c696be remove extension build instructions 2024-11-20 21:43:14 -08:00
Andrew Pareles 077d460b29 huge revamp to get tabs working. tabs work! 2024-11-20 21:17:29 -08:00
Andrew Pareles f3d9893588 change from model.id to model.uri and fix listener leak 2024-11-20 03:14:16 -08:00
Andrew Pareles c004e1af03 add nice visuals for accept | delete and add comments for void 2024-11-20 00:40:59 -08:00
Andrew Pareles 0d227f2bd6 add void comment 2024-11-19 22:51:49 -08:00
Andrew Pareles 0b96901aee update error 2024-11-19 18:13:45 -08:00
Piyush Bhardwaj d12a5f3b38 Merge branch 'main' into Gemini 2024-11-19 18:10:58 +05:30
mp cd77542a9e Autocomplete (max number of pending requests, filter for matchup with user's text, better prompt) 2024-11-19 03:57:06 -08:00
Andrew Pareles 0c2660e283 add history to approve/reject 2024-11-19 02:22:41 -08:00
Andrew Pareles 9a591ba29f accept/reject works!! 2024-11-19 02:21:11 -08:00
Andrew Pareles 4c633ee8ff remove code 2024-11-18 21:02:18 -08:00
Andrew Pareles 67e9759d35 make history get added instantly so we can do interrupts 2024-11-18 18:05:50 -08:00
Andrew Pareles 93dac201b3 Merge pull request #150 from jcommaret/fix-extension-lodash-dep
lodash : package added to extension
2024-11-18 17:57:14 -08:00
Andrew Pareles 946ec70136 sweep works exceptionally! 2024-11-18 17:37:32 -08:00
mp 161682cf75 Merge remote-tracking branch 'origin/autocomplete' 2024-11-18 17:14:26 -08:00
mp 6df0093786 ui 2024-11-18 17:13:55 -08:00
Andrew Pareles a527af93b7 Merge pull request #145 from nathfavour/main
Main
2024-11-18 16:22:05 -08:00
Andrew Pareles 775cb2e4ac Update dev.nix 2024-11-18 16:21:47 -08:00
Piyush Bhardwaj 68b5187f7c Merge branch 'main' into Gemini 2024-11-18 20:13:41 +05:30
Jérôme Commaret aacbef3e33 lodash : package added to extension 2024-11-18 12:58:42 +01:00
Andrew Pareles f5b2dcfe0a use renderLines instead of custom styles (still doesnt compute line wrap properly though) 2024-11-18 01:09:02 -08:00
Andrew Pareles 9d25fab5cb fix linting 2024-11-17 21:40:46 -08:00
Andrew Pareles 250b90634f Merge pull request #142 from plon-Susk7/dev
pass build information to extension
2024-11-17 19:39:35 -08:00
Andrew Pareles e56716c792 move to posthog 2024-11-17 19:39:15 -08:00
Andrew Pareles 0bf5278168 Merge branch 'main' into pr/plon-Susk7/142 2024-11-17 19:35:07 -08:00
Andrew Pareles f2c61d2419 fix formatting 2024-11-17 19:32:06 -08:00
mp bdd224990d Merge branch 'autocomplete' 2024-11-17 16:49:09 -08:00
mp 0ca2ad4930 Create basic language server functionality 2024-11-17 16:48:23 -08:00
Andrew Pareles 5140c79dcf refactor to have openAI conventions 2024-11-17 02:19:21 -08:00
Andrew Pareles 91c65b6060 fix misc 2024-11-17 02:09:39 -08:00
Andrew Pareles afd40a59b5 Merge pull request #149 from dcqe/patch-1
fix windows command
2024-11-17 02:01:36 -08:00
Andrew Pareles e6177d44c6 fix overlapping green styles 2024-11-16 23:13:59 -08:00
Andrew Pareles 63778d2012 add new findDiff test 2024-11-16 23:05:52 -08:00
Andrew Pareles 7a33cde334 fix findDiffs 2024-11-16 22:59:45 -08:00
Andrew Pareles c4e29d1c3f add history back! 2024-11-16 22:30:59 -08:00
Andrew Pareles b6b5deaa58 diffArea stream highlighting works perfectly! 2024-11-16 21:58:56 -08:00
Andrew Pareles 689a583598 diff styling is pretty consistent 2024-11-16 21:55:09 -08:00
Andrew Pareles 9f6cacdbe1 + 2024-11-16 20:49:24 -08:00
Andrew Pareles abab461236 move around 2024-11-16 20:46:39 -08:00
Andrew Pareles 23579774b8 diffareas refresh on user changes 2024-11-16 20:35:46 -08:00
Nico Mayr 0ee85b0ffd fix windows command 2024-11-17 01:34:58 +01:00
Andrew Pareles c99b1a2132 diff works! 2024-11-15 23:30:31 -08:00
Andrew Pareles 8a9e9495f9 realign after write 2024-11-15 22:40:54 -08:00
Andrew Pareles 1437f592d2 BEFORE DELETE RESIZE 2024-11-15 22:34:37 -08:00
Andrew Pareles 4ac509f0c8 update originalCode to live in Diffs 2024-11-15 21:57:23 -08:00
Andrew Pareles f37440cd3f add colors 2024-11-15 20:30:09 -08:00
Andrew Pareles ea83546e42 temp remove addtohistory 2024-11-15 04:38:27 -08:00
Andrew Pareles e2b36d735c update VSReadFile 2024-11-15 04:31:01 -08:00
Andrew Pareles 1ca94080b8 modelService 2024-11-15 04:29:25 -08:00
Andrew Pareles 2ad063eb30 simplify sweep + refresh 2024-11-15 03:42:38 -08:00
Andrew Pareles 2aea2b81c2 restore diffareas properly 2024-11-14 23:42:53 -08:00
Andrew Pareles 3fc81f1baf actions 2024-11-14 22:30:58 -08:00
Andrew Pareles 063d9312c4 fix selection using .bind() 2024-11-14 22:11:41 -08:00
mp 04761c9418 Minor changes 2024-11-14 22:00:05 -08:00
Andrew Pareles 620b335dc5 update code selection code 2024-11-14 21:20:07 -08:00
mp a765d738e1 Basic LSP Usage 2024-11-14 18:08:52 -08:00
Andrew Pareles 878ec1e782 fixing bugs 2024-11-14 05:00:32 -08:00
Andrew Pareles 84d866205b remove bugs 2024-11-14 04:05:23 -08:00
Andrew Pareles a273780472 update services to not use a Provider 2024-11-14 03:38:41 -08:00
Andrew Pareles dbea46ba32 fix services in React 2024-11-14 03:22:37 -08:00
Andrew Pareles 50d21828c2 works!! 2024-11-14 02:17:02 -08:00
Andrew Pareles a974a5df69 works! 2024-11-14 02:09:55 -08:00
Andrew Pareles 799f6c2d16 fix build regex? 2024-11-14 01:58:40 -08:00
Andrew Pareles b8f3182ee3 .js only might work 2024-11-14 01:48:56 -08:00
Andrew Pareles f22bc26355 getting there... 2024-11-14 01:47:33 -08:00
Andrew Pareles 047cc03002 minor updates trying to get it to build 2024-11-14 01:29:54 -08:00
Andrew Pareles bd49aaffde update noExternal 2024-11-14 01:06:15 -08:00
Andrew Pareles 13f6fd5c66 build! 2024-11-14 00:53:06 -08:00
Andrew Pareles 1fe12d2bfd make everything revolve around model, not editor 2024-11-13 23:58:28 -08:00
Andrew Pareles 1cf371fa8b giant updates to registerInlineDiffs 2024-11-13 23:21:54 -08:00
Andrew Pareles ee72a2c3cd registerInlineDiffs progress 2024-11-13 03:25:18 -08:00
Andrew Pareles a5912ba538 progress adding diffs 2024-11-12 03:28:53 -08:00
mp 19a8d36e07 Add LRU cache 2024-11-12 02:31:26 -08:00
mp 339aff5d31 Refactor sendLLMMessage and add FIM mode 2024-11-12 01:36:32 -08:00
Andrew Pareles 4de3f1c7ef start editing registerInlineDiff 2024-11-12 00:40:33 -08:00
Andrew Pareles 6e2030d4d4 finish converting react to VS 2024-11-11 21:14:43 -08:00
nathfavour 665e5e1058 open in idx 2024-11-11 19:53:35 -08:00
Andrew Pareles 8b9a9c7e2b prefix 2024-11-11 03:39:18 -08:00
Andrew Pareles f8f60ef6b0 re-add tailwind for content:['./src2'] tag 2024-11-11 03:38:21 -08:00
Andrew Pareles d01a5fc44c delete tailwind.config 2024-11-11 03:33:16 -08:00
Andrew Pareles f713a624f9 add prefix in tailwind.config, apparently adding it in scope-tailwind doesnt work 2024-11-11 03:29:50 -08:00
Andrew Pareles c789ab2ab7 WORKS 2024-11-11 03:16:15 -08:00
Andrew Pareles 008a5812f3 build should work now 2024-11-11 03:10:17 -08:00
Andrew Pareles 06819e172a add scope 2024-11-11 03:04:48 -08:00
Andrew Pareles 1112f79cf5 update imports and tsup 2024-11-11 02:59:27 -08:00
Andrew Pareles 9a54a57cf4 move everything to src folder 2024-11-11 02:51:10 -08:00
Andrew Pareles c4d620c874 create scope-tailwind 2024-11-11 02:29:36 -08:00
Andrew Pareles 0cc06407ad start adding tailwind 2024-11-11 02:29:10 -08:00
mp aab065139e Trim prefixes to increase cache hit rate 2024-11-10 23:52:25 -08:00
pythons aa6b5c6b6c refactor: modularize LLM providers and types
- Split monolithic sendLLMMessage.ts into separate provider modules
- Create dedicated files for each LLM provider (anthropic, gemini, openai, ollama, greptile)
- Move types into separate types.ts file
- Update import paths across affected files
- No functional changes, purely architectural improvement

This refactoring improves code organization and maintainability by:
- Separating concerns for each LLM provider
- Making the codebase more modular
- Improving type management
2024-11-11 15:10:32 +08:00
Andrew Pareles 9fc3e413a9 services work in react 2024-11-10 19:39:22 -08:00
Andrew Pareles 289da6bd6d fixed linting 2024-11-10 17:14:16 -08:00
Andrew Pareles 1feb7ec485 try to fix linting (update package) 2024-11-10 16:55:19 -08:00
Andrew Pareles 5f97a4f8a6 del out/ 2024-11-10 16:27:19 -08:00
Andrew Pareles 752ebfff3b update externals 2024-11-10 16:26:01 -08:00
Andrew Pareles 01d99645a6 WORKS 2024-11-09 21:03:03 -08:00
Andrew Pareles 00814d51f1 misc updates 2024-11-09 20:23:28 -08:00
Andrew Pareles e8ff4d8328 revert 2024-11-09 20:11:20 -08:00
Andrew Pareles 8da76e72df big changes + react build pipeline (broken) 2024-11-09 19:57:57 -08:00
mp 652f64cd14 Add basic system prompt 2024-11-08 21:57:33 -08:00
Andrew Pareles b52f0d8dd7 misc 2024-11-08 17:29:40 -08:00
priyash7 f8917e7548 added strings to info 2024-11-08 17:51:56 +05:30
Andrew Pareles 932e9fe9c6 progress 2024-11-08 02:50:52 -08:00
Andrew Pareles 3fd4357503 progress migrating to native vscode 2024-11-08 02:23:11 -08:00
mp 9b878a9fc0 Merge remote-tracking branch 'origin/main' into autocomplete 2024-11-08 01:28:19 -08:00
mp 268d03fad5 Add working autocomplete with caching 2024-11-08 01:23:09 -08:00
Andrew Pareles 3aece81891 move void files around 2024-11-06 23:18:45 -08:00
Andrew Pareles 6b268a44bb backup again 2024-11-06 21:46:17 -08:00
Andrew Pareles 70ce23862b back up build 2024-11-06 20:50:09 -08:00
Andrew Pareles 1c32d3a1c9 update build 2024-11-06 19:45:02 -08:00
Andrew Pareles 2814c63765 drag and drop extension into main 2024-11-06 16:01:59 -08:00
Piyush Bhardwaj cc3a08e1ab Merge branch 'main' into Gemini 2024-11-06 22:11:38 +05:30
Piyu-Pika 5b3a6d240e minor bug fix 2024-11-06 22:04:46 +05:30
priyash7 cc8c4d33ef passed info 2024-11-06 17:25:35 +05:30
Andrew Pareles ada31ccf90 update build to work in general 2024-11-06 01:41:26 -08:00
Andrew Pareles 4a629be2e2 add build script hack 2024-11-05 23:41:55 -08:00
Piyu-Pika 8897feb290 Groq Added 2024-11-06 12:21:11 +05:30
Andrew Pareles 75f97ea9e1 update streaming state 2024-11-05 18:47:40 -08:00
Andrew Pareles 83b9547701 add compilation of openai so we can use it internally 2024-11-05 18:38:30 -08:00
Andrew Pareles 791f6b1151 Merge pull request #140 from Madhav160804/ollama-model-selection
Added Ollama Model Check for installed models
2024-11-04 14:44:29 -08:00
Madhav160804 1c739e521d ollama error handling 2024-11-05 02:49:24 +05:30
Madhav Dhingra 794f35fc37 added ollama model check 2024-11-05 02:30:30 +05:30
Andrew 6c93028ce3 misc 2024-11-03 19:39:57 -08:00
Andrew 49072d43ea minor changes 2024-11-03 18:06:40 -08:00
Andrew a5090b8025 simplify 2024-11-03 01:57:05 -08:00
Andrew 36c3cd53e1 add new logging algo and move applyDiffLazily to diffProvider 2024-11-03 00:31:32 -07:00
Andrew e4e1c7fb6c vscode types in extension 2024-11-02 21:53:08 -07:00
Andrew fcc8de807f Merge branch 'main' into actual-editor-insets 2024-11-02 21:26:14 -07:00
Andrew 9d972f72ea update and add imports of sendLLMMessage 2024-11-02 21:25:39 -07:00
Andrew 3d629396be backup of a working version 2024-11-02 20:42:08 -07:00
Mathew Pareles fe413cb474 Update install instructions 2024-11-02 18:38:33 -07:00
Andrew Pareles 277f99e080 Merge pull request #137 from eltociear/patch-2
docs: update .devcontainer/README.md
2024-11-02 02:51:05 -07:00
Andrew 584d026256 renders oldText nicely! 2024-11-02 00:50:18 -07:00
Andrew 5acc53311d add files to make native insets work! #124 2024-11-01 21:23:38 -07:00
Ikko Eltociear Ashimine b9ba3a36c4 docs: update .devcontainer/README.md
minor fix
2024-11-01 17:16:57 +09:00
mp d7c00addd5 update inline diffs 2024-10-31 17:10:39 -07:00
Andrew Pareles b34119c4df Merge pull request #134 from Piyu-Pika/Gemini
Gemini added #new feature
2024-10-30 23:19:52 -07:00
Andrew Pareles d098291c90 minor note 2024-10-30 23:19:34 -07:00
Andrew ed9e126a4a remove duplicate extension.ts 2024-10-30 23:16:36 -07:00
Andrew a8010eec15 simplify gemini 2024-10-30 22:46:47 -07:00
Piyu-Pika 8cd1ea305c Gemini 2024-10-31 10:41:04 +05:30
Piyush Bhardwaj 762264b030 Merge branch 'main' into Gemini 2024-10-31 10:28:17 +05:30
Andrew Pareles 48d5369196 amend 2024-10-30 19:09:43 -07:00
Andrew Pareles aab840db2d amend 2024-10-30 19:02:48 -07:00
Andrew Pareles 3526005e1c amend 2024-10-30 19:02:22 -07:00
Andrew Pareles 0ef8c66d54 simplify readme 2024-10-30 19:01:56 -07:00
Andrew cd9173c802 move ctrlk 2024-10-30 18:59:17 -07:00
Piyu-Pika 0c8a185df7 Gemini added 2024-10-29 18:05:07 +05:30
Andrew 0f0719c596 fix type 2024-10-29 00:26:46 -07:00
Andrew d9397d870f change CodeSelection type 2024-10-28 17:17:26 -07:00
Andrew 1704a7de7a improve line rounding 2024-10-28 16:34:19 -07:00
Andrew 616cd2e39b update back to enum 2024-10-28 15:58:48 -07:00
Andrew Pareles 222f90bbea Merge pull request #133 from voideditor/multiple-webviews
Max tokens is optional + other config updates - Fixes #128
2024-10-28 15:50:40 -07:00
Andrew ab5245d2d9 Max tokens is optional + other config updates - Fixes #128 2024-10-28 15:50:05 -07:00
Andrew Pareles a7a4488848 minor Contributing changes 2024-10-28 04:22:19 -07:00
Andrew Pareles 8e72c1392a Merge pull request #125 from voideditor/multiple-webviews
Speculative Edits
2024-10-28 03:59:53 -07:00
Andrew 70b6af34e1 Merge branch 'main' into multiple-webviews 2024-10-28 03:58:36 -07:00
Andrew 119462f663 fix imports?? 2024-10-28 03:58:09 -07:00
Andrew 8ee665cd57 fix imports 2024-10-28 03:53:40 -07:00
Andrew ace3f181ed Merge branch 'speculative-edits' into multiple-webviews 2024-10-28 03:53:02 -07:00
Andrew 6d12737fda add throttling 2024-10-28 03:52:19 -07:00
Andrew d3962acd30 merge - but appears broken 2024-10-28 03:44:13 -07:00
Andrew 18557c8f6e comment out ctrlk so it builds 2024-10-28 03:37:18 -07:00
Andrew 29b037726e Merge branch 'speculative-edits' into multiple-webviews 2024-10-28 03:36:39 -07:00
mp eae1af331f Create Ctrl + K 2024-10-28 03:36:07 -07:00
Andrew 2e92da500f lines sweep works! Need to polish it. 2024-10-28 03:19:40 -07:00
Andrew 59dc193f97 update contrib 2024-10-26 21:36:45 -07:00
Andrew Pareles bd778d259b fix typo 2024-10-26 21:33:12 -07:00
Andrew 605cf236a5 link issues 2024-10-26 19:17:53 -07:00
Andrew 8bc3a0d277 windows build works!! 2024-10-26 19:17:20 -07:00
Andrew d80c88bc38 move files to right places 2024-10-25 22:39:13 -07:00
Andrew dd34a5be57 fix ctrl L change 2024-10-25 22:35:13 -07:00
Andrew f4be80b724 Merge remote-tracking branch 'origin/ctrlk-diff-ui' into multiple-webviews 2024-10-25 22:31:44 -07:00
Andrew b40c58b78a Merge remote-tracking branch 'origin/speculative-edits' into multiple-webviews 2024-10-25 22:25:47 -07:00
Andrew 64468b6f4d Merge branch 'speculative-edits' into multiple-webviews 2024-10-25 22:14:11 -07:00
Andrew 8f83b063f1 add ctrlk webview provider 2024-10-25 22:10:22 -07:00
mp 9eb897b655 Better file reading function 2024-10-25 21:55:15 -07:00
mp a738ca0b2c Reafactor DiffProvider for ctrl+k 2024-10-25 21:47:00 -07:00
Andrew f56772a07a amend 2024-10-25 19:58:35 -07:00
Andrew 38662d2824 major refactor, allow 3 types of webview 2024-10-25 19:49:59 -07:00
Andrew a51873c06a move files to extension/ 2024-10-25 15:58:06 -07:00
Andrew a29543e4a7 Promisify 2024-10-25 01:33:54 -07:00
Andrew d4a2ac2796 draft 2024-10-25 01:22:10 -07:00
Andrew 4b5e09c124 Merge remote-tracking branch 'origin/speculative-edits' into ctrlk-diff-ui 2024-10-24 21:08:57 -07:00
Andrew f8f48fb98b + 2024-10-24 20:48:04 -07:00
Andrew 1b8e7f3643 amend 2024-10-24 19:59:25 -07:00
Andrew 420ecb320e Merge branch 'main' into speculative-edits 2024-10-24 19:58:58 -07:00
mp 097f279ecb Throttle edit requests, refactor 2024-10-24 19:48:31 -07:00
Andrew 9a1d4e2b9c comment 2024-10-24 19:34:09 -07:00
Andrew a926efcd2a attempt windows build 2024-10-24 19:25:20 -07:00
Andrew Pareles 40aa319d18 update react comment 2024-10-24 15:37:13 -07:00
Andrew Pareles 549dabbde6 X [ERROR] Cannot start service: Host version "0.23.1" 2024-10-24 15:35:43 -07:00
Andrew Pareles 42e52647f3 Merge pull request #121 from jcommaret/main
Doc : Build command fix
2024-10-23 11:33:09 -07:00
Andrew Pareles a12ffd6b5e Update CONTRIBUTING.md 2024-10-23 11:32:52 -07:00
Andrew Pareles 3d74e77760 Update CONTRIBUTING.md 2024-10-23 11:32:27 -07:00
Jérôme Commaret 0ffd66e747 Doc : Build command fix 2024-10-23 12:30:57 +02:00
Andrew Pareles e78fa459cb Merge pull request #120 from voideditor/@andrewpareles/editor-insets
discord
2024-10-23 03:17:52 -07:00
Andrew c01b1e9488 discord 2024-10-23 03:16:10 -07:00
Andrew Pareles 45c7b951e9 or just run 2024-10-23 01:48:39 -07:00
Andrew Pareles f31f5e7ad1 Merge pull request #119 from voideditor/@andrewpareles/editor-insets
Editor Insets
2024-10-23 01:46:54 -07:00
Andrew Pareles c3a934777d Update CONTRIBUTING.md 2024-10-23 01:46:30 -07:00
Andrew Pareles 3aba62a6ed update contrib + ctrlk hello world works! 2024-10-23 01:40:47 -07:00
Andrew Pareles ad5128f885 amend 2024-10-23 00:51:31 -07:00
Andrew Pareles 376172e5a9 update contributing 2024-10-23 00:49:33 -07:00
Andrew Pareles 4dd6553e34 building on a mac works! so does editorInset! 2024-10-22 23:49:03 -07:00
Andrew f2b3a2be30 comment out checkProposedApiEnabled(extension, 'editorInsets'); 2024-10-22 22:59:09 -07:00
mp 1e4c932313 Track call time 2024-10-22 14:32:39 -07:00
mp 7bd9e01a15 Working draft of speculative edits 2024-10-21 18:33:00 -07:00
Andrew 69277a287a update Ollama message 2024-10-21 15:24:53 -07:00
Andrew Pareles 6c0f17d8f9 Merge pull request #113 from voideditor/monaco-editor
Misc Improvements
2024-10-21 14:45:30 -07:00
Andrew 6dc9988af0 amend 2024-10-20 23:28:51 -07:00
Andrew 18656372f8 add ctrlk, autofocus chatbar 2024-10-20 23:24:40 -07:00
Andrew febf492790 clarify 2024-10-20 20:15:25 -07:00
Andrew bcb0c33616 amend 2024-10-20 19:23:47 -07:00
Andrew Pareles 7072933a88 Update build-tsx.js 2024-10-20 19:20:03 -07:00
Andrew f2342bee2d add back syntax highlighter 2024-10-20 18:39:53 -07:00
Andrew 572e9f07ea misc 2024-10-20 17:14:10 -07:00
Andrew 09d84044b5 monaco loads... need to adjust height 2024-10-19 21:38:17 -07:00
mp 9ecf596cbb Prepare to debug 2024-10-19 20:17:54 -07:00
Andrew a49a1324a5 Merge branch 'main' into monaco-editor 2024-10-19 18:50:05 -07:00
Andrew 8538caeffc start adding monaco 2024-10-19 18:49:43 -07:00
Andrew 79ecd98818 add metrics 2024-10-19 18:21:19 -07:00
mp f6cad40a44 Finish speculative draft 2024-10-18 19:18:11 -07:00
mp e25368ef7f Merge branch 'main' into speculative-edits 2024-10-18 17:25:13 -07:00
mp 7ce17a0854 Initial draft for speculative edits 2024-10-18 17:19:38 -07:00
Andrew c5b5e07a98 monaco comment 2024-10-18 00:48:44 -07:00
Andrew 2addd06e60 switch to new tab on ctrl+l 2024-10-17 23:26:31 -07:00
Andrew 177f2e92f8 amend - fix disabled 2024-10-17 21:34:30 -07:00
Andrew 217f61902e add metrics 2024-10-17 21:23:07 -07:00
Andrew 9274602051 amend - uuid type 2024-10-17 19:15:42 -07:00
Andrew Pareles efa3b033af Merge pull request #110 from voideditor/ui-refactor-10-15
Settings update
2024-10-17 17:31:26 -07:00
Andrew 18649d3255 fix package 2024-10-17 17:29:28 -07:00
Andrew 2b55d94cce Merge branch 'main' into ui-refactor-10-15 2024-10-17 17:26:08 -07:00
Andrew Pareles 2d17133684 Merge pull request #87 from w1gs/main
Added displaying error messages in UI
2024-10-17 17:18:56 -07:00
Andrew e2e6cb0327 Merge branch 'main' into pr/w1gs/87 2024-10-17 17:18:12 -07:00
Andrew 5415f1e3cb amend 2024-10-17 16:46:52 -07:00
Andrew 0f3a79ab54 error UI 2024-10-17 16:43:25 -07:00
Andrew 983e926a23 add error handling from #87 2024-10-17 16:36:37 -07:00
Andrew 302aa2b384 warning for devs 2024-10-17 15:30:01 -07:00
Andrew 84ff9784ca fix sidebar settings 2024-10-17 01:20:59 -07:00
Andrew Pareles 6dfab6d00c Merge pull request #77 from SOUMITRO-SAHA/soumitrasaha#ui
Fix: Add Syntax Highlighting to Sidebar Code Snippets
2024-10-17 00:17:58 -07:00
Andrew 628ed9a578 Merge branch 'main' into pr/SOUMITRO-SAHA/77 2024-10-17 00:15:15 -07:00
Andrew 0a6f7b8ebc Merge branch 'main' into ui-refactor-10-15 2024-10-17 00:12:03 -07:00
Andrew Pareles 5b511edca1 Merge pull request #108 from IDLe-Engineering/patch-3
Connect-src updates for OpenAICompatible
2024-10-17 00:11:25 -07:00
Andrew Pareles 4ecbd998bd Merge pull request #106 from IDLe-Engineering/patch-2
Build prompt dynamically
2024-10-17 00:10:54 -07:00
Andrew fcd0eb30c6 fix 2024-10-16 23:06:34 -07:00
Andrew aa4ce3478a update styles in settings page 2024-10-16 23:03:51 -07:00
Andrew 1e82781c52 outline color 2024-10-16 20:42:47 -07:00
Andrew edf9dc9279 thread switching 2024-10-16 20:03:50 -07:00
Andrew 63c6b6bec5 vscode styles 2024-10-16 20:00:07 -07:00
Andrew 154a82ab0b add settings UI 2024-10-16 19:32:21 -07:00
Andrew Pareles ab51e97008 Merge pull request #109 from IDLe-Engineering/patch-4
Add Syntax Highlighting to Sidebar Code Snippets
2024-10-16 16:23:18 -07:00
Andrew Pareles 0240fc1803 Update package.json 2024-10-16 16:22:59 -07:00
Andrew d3b862ae91 history display slightly nicer 2024-10-16 16:19:32 -07:00
Andrew dc2344fd4c Merge branch 'main' into ui-refactor-10-15 2024-10-16 14:53:03 -07:00
Andrew 0072c554fb tabs 2024-10-16 14:52:49 -07:00
Tom Spielvogel ed8381c6cb Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:29:46 +02:00
Tom Spielvogel 40b4ecb4f8 Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:29:27 +02:00
Tom Spielvogel 5268ededbb Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:28:48 +02:00
Tom Spielvogel a69e1b940f Add Syntax Highlighting to Sidebar Code Snippets 2024-10-16 13:26:24 +02:00
Tom Spielvogel 15994c46d1 Adapted to new type CodeSelection 2024-10-16 11:28:23 +02:00
Tom Spielvogel b565a91194 Connect-src needs hold the entire path for chat completions for OpenAICompatible
Simply using the enpoint will trigger a Content-Security-Policy error in vscode when trying to send messages to the LLM
2024-10-16 10:56:46 +02:00
Andrew b30937934a progress moving settings to be extension-native 2024-10-16 01:12:57 -07:00
Tom Spielvogel eec7cd7321 Update SidebarChat.tsx
Build the prompt based on the actual use-case.
If no files or code is selected, don't prompt the LLM to change it. It saves time and tokens
2024-10-16 08:55:37 +02:00
Andrew Pareles f86b84dcc6 Merge pull request #105 from voideditor/ui-refactor-10-15
Split up Sidebar tabs into files and clearer message types (incorporates direction)
2024-10-15 20:54:33 -07:00
Andrew cd9bca0451 split up message direction and Sidebar tabs 2024-10-15 20:51:46 -07:00
Darion ce3043b595 Merge branch 'main' into main 2024-10-15 21:36:00 -04:00
Andrew Pareles 68c5ebb000 Merge pull request #101 from voideditor/diff-selections
Refactor and fix diff highlighting
2024-10-15 15:57:33 -07:00
w1gs 08e91e921b style fixes 2024-10-15 12:36:38 -04:00
w1gs 6c2ca794f5 fixed merge conflicts 2024-10-15 12:24:26 -04:00
Darion b3fd408cf7 Merge branch 'main' into main 2024-10-15 11:09:32 -04:00
Andrew Pareles dc96d126bb add package.json to list of void's changed files 2024-10-15 00:31:40 -07:00
Andrew 07fd622901 add OpenRouter - Fixes Add OpenRouter hosting option #20 2024-10-14 23:50:34 -07:00
Andrew bb74cbb56b swap 2024-10-14 23:32:25 -07:00
Darion ef94de48c2 Merge branch 'main' into main 2024-10-11 12:56:08 -04:00
w1gs 14a8b0ee4e fixed merge conflicts 2024-10-11 12:52:12 -04:00
Darion c3e2dc1621 Merge branch 'voideditor:main' into main 2024-10-05 16:53:40 -04:00
w1gs bed7929e48 Added error messages in UI 2024-10-03 21:24:53 -04:00
SOUMITRO-SAHA b8a5e5b67c Fix: Add Syntax Highlighting to Sidebar Code Snippets 2024-09-29 20:49:59 +05:30
133 changed files with 17466 additions and 14643 deletions
+1 -1
View File
@@ -56,7 +56,7 @@ Next: **[Try it out!](#try-it)**
You may see improved VNC responsiveness when accessing a codespace from VS Code client since you can use a [VNC Viewer][def]. Here's how to do it.
1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces).
1. Install [Visual Studio Code Stable](https://code.visualstudio.com/) or [Insiders](https://code.visualstudio.com/insiders/) and the [GitHub Codespaces extension](https://marketplace.visualstudio.com/items?itemName=GitHub.codespaces).
> **Note:** The GitHub Codespaces extension requires the Visual Studio Code distribution of Code - OSS.
+1
View File
@@ -10,6 +10,7 @@
"jsdoc",
"header",
"local"
// "react" // Void
],
"rules": {
"constructor-super": "warn",
+1
View File
@@ -21,3 +21,4 @@ vscode.db
product.overrides.json
*.snap.actual
.vscode-test
.tmp/
+48
View File
@@ -0,0 +1,48 @@
# Created for Void
# To learn more about how to use Nix to configure your environment
# see: https://developers.google.com/idx/guides/customize-idx-env
{pkgs}: {
# Which nixpkgs channel to use.
channel = "stable-23.11"; # or "unstable"
# Use https://search.nixos.org/packages to find packages
packages = [
pkgs.nodejs_20
pkgs.yarn
pkgs.nodePackages.pnpm
pkgs.bun
pkgs.gh
];
# Sets environment variables in the workspace
env = {};
idx = {
# Search for the extensions you want on https://open-vsx.org/ and use "publisher.id"
extensions = [
# "vscodevim.vim"
];
workspace = {
# Runs when a workspace is first created with this `dev.nix` file
onCreate = {
npm-install = "npm ci --no-audit --prefer-offline --no-progress --timing";
# Open editors for the following files by default, if they exist:
default.openFiles = [
# Cover all the variations of language, src-dir, router (app/pages)
"pages/index.tsx" "pages/index.jsx"
"src/pages/index.tsx" "src/pages/index.jsx"
"app/page.tsx" "app/page.jsx"
"src/app/page.tsx" "src/app/page.jsx"
];
};
# To run something each time the workspace is (re)started, use the `onStart` hook
};
# Enable previews and customize configuration
previews = {
enable = true;
previews = {
web = {
command = ["npm" "run" "dev" "--" "--port" "$PORT" "--hostname" "0.0.0.0"];
manager = "web";
};
};
};
};
}
+93 -115
View File
@@ -1,163 +1,141 @@
# Contributing to Void
Welcome! 👋 This is a guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
Welcome! 👋 This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
There are two main ways to contribute:
There are a few ways to contribute:
- Suggest New Features ([discord](https://discord.gg/RSNjgaugJs))
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
- 👨‍💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
## 1. Building the Extension
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
1. Clone the repository:
## Building the full IDE
### a. Build Prerequisites - Mac
If you're using a Mac, you need Python and XCode. You probably have these by default.
### b. Build Prerequisites - Windows
If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them.
Go to the "Workloads" tab and select:
- `Desktop development with C++`
- `Node.js build tools`
Go to the "Individual Components" tab and select:
- `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`
- `C++ ATL for latest build tools with Spectre Mitigations`
- `C++ MFC for latest build tools with Spectre Mitigations`
Finally, click Install.
### c. Build Prerequisites - Linux
First, run `npm install -g node-gyp`. Then:
- Debian (Ubuntu, etc): `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
### Building Void
To build Void, open `void/` inside VSCode. Then:
1. `npm install` to install all dependencies.
2. `npm run watchreact` to build Void's browser dependencies like React.
3. Build.
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
- This step can take ~5 min. The build is done when you see two check marks.
4. Run.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
#### Building Void from Terminal
Alternatively, if you want to build Void from the terminal, instead of pressing <kbd>Cmd+Shift+B</kbd> you can run `npm run watch`. The build is done when you see something like this:
```
git clone https://github.com/voideditor/void
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
[watch-client ] [00:38:06] Finished compilation with 0 errors after 46248 ms
[watch-client ] [00:38:07] Starting compilation...
[watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms
```
2. Open the folder `/extensions/void` in VS Code (open it in a new workspace, *don't* just cd into it):
```
open /extensions/void
```
3. Install dependencies:
### Common Fixes
```
npm install
```
- Make sure you follow the prerequisite steps.
- Make sure you have the same NodeJS version as `.nvmrc`.
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
4. Build the project. We created this build command so that we could run React in vscode - it converts `sidebar/index.tsx` into a CSS/JS bundle in `dist/`.
```
npm run build
```
5. Run the project by pressing <kbd>F5</kbd>.
## Bundling
This will start a new instance of VS Code with the extension enabled. If this does not work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VS Code Extension Development".
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` (or similar) in the repo's parent's directory. Be patient - compiling can take ~25 minutes.
If you would like to use AI features, you need to provide an API key. You can do that by going to Settings (Ctrl+,) typing in "void", and adding the API key you want to use (eg. the `"Anthropic Api Key"` environment variable). The "Which API" environment variable controls the provider and defaults to "anthropic".
### Mac
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
- `npm run gulp vscode-darwin-x64` (Intel)
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
### Windows
- `npm run gulp vscode-win32-x64` - most common
- `npm run gulp vscode-win32-ia32`
## 2. Building the full IDE
### Linux
- `npm run gulp vscode-linux-x64` - most common
- `npm run gulp vscode-linux-arm`
- `npm run gulp vscode-linux-ia32`
Beyond the extension, we very occasionally edit the IDE when we need to access more functionality. If you want to work on the full IDE, please follow the steps below, or see VS Code's full [how to contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`). Also make sure you have Python on your system.
Make sure you're on the correct NodeJS version as per `.nvmrc`.
1. Install all dependencies.
```
npm install
```
2. In VS Code, press <kbd>Ctrl+Shift+B</kbd> to start the build process - this can take some time. If you're not using VS Code, run `npm run watch` instead.
3. Run `./scripts/code.sh` in your terminal.
This should open up the built IDE after loading for some time. To see new changes without restarting the build, use <kbd>Ctrl+Shift+P</kbd> and run "Reload Window".
To bundle the IDE, run `npm run gulp vscode-darwin-arm64`. Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)`
If you're on Windows, we recommend running the project inside a dev container. VSCode should prompt you to do this automatically.
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
## Roadmap
Here are the most important topics on our Roadmap. More ⭐'s = more important.
## ⭐⭐⭐ Improve diffs.
We define a "diff" as a single green/red pair that denotes a change. Here are improvements to make:
1. Show deletion (-) diffs. Right now we're only showing insertion (+) diffs. Diffs currently work by highlighting all of the new code in green with a simple text decoration. Instead, we would like to use code from VS Code's native diffEditor to show the diffs ("inline" mode). We could alternatively keep what we have and add red zones of the deleted code to indicate a deletion diff (-).
2. Fix bugginess when the user presses "Accept" or "Reject" on a diff. One issue is that when a diff is accepted/rejected all of the diffs below should be updated (because they are now on different line numbers). There are other miscellaneous issues too.
3. Make diff highlighting dynamic. Right now when the user edits text, all of the diffs and their highlights are cleared. Instead, we should update the highlighting of the diff. Each diff lives on a range of lines, and all changes inside that range or intersecting with it should update its highlighting.
## ⭐⭐⭐ Build Cursor-style quick edits (ctrl+k).
When the user presses ctrl+k, an input box should appear inline with the code that they were selecting. This is somewhat difficult to do because an extension alone cannot do this, and it requires creating a new component in the IDE. We think you can modify vscode's built-in "codelens" or "zone widget" components, but we are open to alternatives.
## ⭐⭐⭐ Make History work well.
When the user submits a response or presses the apply/accept/reject button, we should add these events to the history, allowing the user to undo/redo them. Right now there is unexpected behavior if the user tries to undo or redo their changes.
## ⭐⭐⭐ Improve Ctrl+L backend.
Right now, the model outputs entire files. Instead, we should change the prompt so that the model outputs partial changes like `// ... rest of file`. When the user clicks the "Apply" button, the model should rewrite the file and apply the partial changes in the correct locations.
## ⭐⭐ Integrate with Ollama.
We have an Ollama integration coded up in the extension, but it breaks. This is because Ollama has Node.js dependencies like 'path' and 'os' which cannot run in extensions (extensions have to be able to run in the browser). To fix this, we need to migrate Void's extension so that it runs natively into the VS Code editor so that we can access Node.js.
## ⭐⭐⭐ Creative.
Feel free to build AI features beyond the standard Cursor ones. For example, creating better code search, or supporting AI agents that can edit across files and make multiple LLM calls.
Eventually, we want to build a convenient API for creating AI tools. The API will provide methods for creating the UI (showing an autocomplete suggestion, or creating a new diff), detecting event changes (like `onKeystroke` or `onFileOpen`), and modifying the user's file-system (storing indexes associated with each file), making it much easier to make your own AI plugin. We plan on building these features further along in timeline, but we wanted to list them for completeness.
## ⭐ One-stars.
⭐ When user presses ctrl+L it should clear the sidebar's state.
⭐ Let the user accept / reject all Diffs in an entire file via the sidebar.
⭐ Allow the user to make multiple selections of code or files at once.
⭐ Allow user to X out of their current selection.
# Guidelines
Please don't make big refactors without speaking with us first. We'd like to keep the codebase similar to vscode so we can periodically rebase, and if we have big changes that gets complicated.
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs).
# Submitting a Pull Request
Please submit a pull request once you've made a change. Here are a few guidelines:
## Submitting a Pull Request
- A PR should be about one *single* feature change. The fewer items you change, the more likely the PR is to be accepted.
- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature.
- Please don't use AI to write your PR 🙂.
- Your PR should contain a description that first explains at a high level what you did, and then describes the exact changes you made (and to which files). Please don't use vague statements like "refactored code" or "improved types" (instead, describe what code you refactored, or what types you changed).
- Your title should clearly describe the change you made.
- Add tags to help us stay organized!
- Please don't open a new Issue for your PR. Just submit the PR.
- Avoid refactoring and making feature changes in the same PR.
- Write good code. For example, a common mistake when people edit Void's config is to hard-code a default value like `'claude-3.5'` in 2+ separate places. Please follow best practices or describe your thought process if you had to compromise.
<!--
# Relevant files
We keep track of all the files we've changed with Void so it's easy to rebase:
- README.md
- CONTRIBUTING.md
- VOID_USEFUL_LINKS.md
- product.json
- package.json
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
- src/vs/workbench/workbench.common.main.ts
- src/vs/workbench/contrib/void
- extensions/void
- src/vs/workbench/contrib/void/\*
- extensions/void/\*
- .github/
- .vscode/settings
- .github/\*
- .vscode/settings/\*
- .eslintrc.json
- build/hygiene.js
- build/lib/i18n.resources.json
- build/npm/dirs.js
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
-->
## References
For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
+5 -11
View File
@@ -1,22 +1,16 @@
# Welcome to Void.
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
Void is the open-source Cursor alternative.
If you're new, welcome! Feel free to check out our [Project board](https://github.com/orgs/voideditor/projects/2/views/3) for the most pressing Issues to work on, and see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) for instructions on building and running Void.
This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
If you're new, welcome!
## Contributing
To build and run Void, see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
To build and run Void, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
## Reference
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository.
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
## Support
Feel free to reach out in our [Discord](https://discord.gg/PspNkKG5wt) or contact us via email.
Feel free to reach out in our [Discord](https://discord.gg/RSNjgaugJs) or contact us via email.
+9 -5
View File
@@ -2,23 +2,27 @@
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
## Contributing
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
## Beginners / Getting started
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
## Contributing
- [How VS Code'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.
## Misc
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - sometimes useful to reference.
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
## VSCode's Extension API
Void is mainly an extension right now, and these links were very useful for us to get set up.
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).
+5 -2
View File
@@ -7,8 +7,6 @@ const fs = require('fs');
// Complete list of directories where npm should be executed to install node modules
const dirs = [
'extensions/void', // <-- Void
'',
'build',
'extensions',
@@ -55,6 +53,11 @@ const dirs = [
'test/smoke',
'.vscode/extensions/vscode-selfhost-import-aid',
'.vscode/extensions/vscode-selfhost-test-provider',
// Void added these:
// 'extensions/void',
// 'void-imports',
];
if (fs.existsSync(`${__dirname}/../../.build/distro/npm`)) {
+18
View File
@@ -139,3 +139,21 @@ for (let dir of dirs) {
cp.execSync('git config pull.rebase merges');
cp.execSync('git config blame.ignoreRevsFile .git-blame-ignore-revs');
// // Void added this (inject void-imports into project):
// const buildVoidImports = () => {
// console.log('\n\nVoid is injecting void-imports...')
// cp.execSync(`npm install`, { // this goes here, not in postinstall, because we need to
// env: process.env,
// cwd: path.join(__dirname, '..', '..', '/void-imports'),
// stdio: 'inherit'
// });
// cp.execSync(`node build-index.mjs`, {
// env: process.env,
// cwd: path.join(__dirname, '..', '..', '/void-imports'),
// stdio: 'inherit'
// });
// console.log('Done injecting void-imports.')
// }
// buildVoidImports()
-56
View File
@@ -1,56 +0,0 @@
{
"root": true,
"parser": "@typescript-eslint/parser",
"parserOptions": {
"ecmaVersion": 6,
"sourceType": "module",
"ecmaFeatures": {
"jsx": true
}
},
"plugins": [
"@typescript-eslint",
"react",
"react-hooks"
],
"extends": [
"eslint:recommended",
"plugin:react/recommended",
"plugin:react-hooks/recommended"
],
"rules": {
"@typescript-eslint/naming-convention": [
"warn",
{
"selector": "import",
"format": [
"camelCase",
"PascalCase"
]
}
],
"curly": "off",
"eqeqeq": "warn",
"no-empty": "off",
"no-throw-literal": "warn",
"semi": "off",
"no-unused-vars": "off",
"react-hooks/exhaustive-deps": "warn"
},
"ignorePatterns": [
"out",
"dist",
"**/*.d.ts"
],
"settings": {
"react": {
"version": "detect"
}
},
"env": {
"browser": true, // enable browser globals linting (window, document, console, etc)
"es6": true, // enable ES6 linting
"node": true, // enable Node linting (things like Buffer which is used in file reading, etc)
"mocha": true // enable Mocha linting
}
}
-5
View File
@@ -1,5 +0,0 @@
out
dist
node_modules
.vscode-test/
*.vsix
-5
View File
@@ -1,5 +0,0 @@
import { defineConfig } from '@vscode/test-cli';
export default defineConfig({
files: 'out/test/**/*.test.js',
});
-8
View File
@@ -1,8 +0,0 @@
{
// See http://go.microsoft.com/fwlink/?LinkId=827846
// for the documentation about the extensions.json format
"recommendations": [
"dbaeumer.vscode-eslint",
"ms-vscode.extension-test-runner"
]
}
-21
View File
@@ -1,21 +0,0 @@
// A launch configuration that compiles the extension and then opens it inside a new window
// Use IntelliSense to learn about possible attributes.
// Hover to view descriptions of existing attributes.
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
{
"version": "0.2.0",
"configurations": [
{
"name": "Run Extension",
"type": "extensionHost",
"request": "launch",
"args": [
"--extensionDevelopmentPath=${workspaceFolder}"
],
"outFiles": [
"${workspaceFolder}/out/**/*.js"
],
"preLaunchTask": "${defaultBuildTask}"
}
]
}
-18
View File
@@ -1,18 +0,0 @@
// Place your settings in this file to overwrite default and user settings.
{
"files.exclude": {
"**/.git": true,
"**/.svn": true,
"**/.hg": true,
"**/CVS": true,
"**/.DS_Store": true,
"**/Thumbs.db": true,
"out": false,
"**/node_modules": true
},
"search.exclude": {
"out": true // set this to false to include "out" folder in search results
},
// Turn off tsc task auto detection since we have the necessary tasks as npm scripts
"typescript.tsc.autoDetect": "off",
}
-20
View File
@@ -1,20 +0,0 @@
// See https://go.microsoft.com/fwlink/?LinkId=733558
// for the documentation about the tasks.json format
{
"version": "2.0.0",
"tasks": [
{
"type": "npm",
"script": "watch",
"problemMatcher": "$tsc-watch",
"isBackground": true,
"presentation": {
"reveal": "never"
},
"group": {
"kind": "build",
"isDefault": true
}
}
]
}
-11
View File
@@ -1,11 +0,0 @@
.vscode/**
.vscode-test/**
src/**
.gitignore
.yarnrc
vsc-extension-quickstart.md
**/tsconfig.json
**/.eslintrc.json
**/*.map
**/*.ts
**/.vscode-test.*
@@ -0,0 +1,333 @@
import * as vscode from 'vscode';
import Parser from 'tree-sitter';
import JavaScript from 'tree-sitter-javascript';
interface Definition {
file: string;
node: Parser.SyntaxNode;
}
interface DefnUse {
parent: Parser.SyntaxNode;
file: string;
}
interface ImportInfo {
source: string;
imported: string;
}
class ProjectAnalyzer {
private parser: Parser;
private graph: Map<string, Set<string>>;
private visited: Set<string>;
private parsedFiles: Map<string, Parser.Tree>;
private imports: Map<string, Map<string, ImportInfo>>;
private definitions: Map<string, Definition>;
private fileStack: Set<string>;
constructor() {
this.parser = new Parser();
this.parser.setLanguage(JavaScript);
this.graph = new Map();
this.visited = new Set();
this.parsedFiles = new Map();
this.imports = new Map();
this.definitions = new Map();
this.fileStack = new Set();
}
async parseFile(filePath: string): Promise<Parser.Tree | null> {
if (this.parsedFiles.has(filePath)) {
return this.parsedFiles.get(filePath)!;
}
if (this.fileStack.has(filePath)) {
return null; // Circular import
}
this.fileStack.add(filePath);
try {
const uri = vscode.Uri.file(filePath);
const document = await vscode.workspace.openTextDocument(uri);
const code = document.getText();
const tree = this.parser.parse(code);
this.parsedFiles.set(filePath, tree);
this.collectImports(filePath, tree);
this.collectDefinitions(filePath, tree);
return tree;
} catch (error) {
console.error(`Error parsing ${filePath}:`, error);
return null;
} finally {
this.fileStack.delete(filePath);
}
}
private collectImports(filePath: string, tree: Parser.Tree): void {
const fileImports = new Map<string, ImportInfo>();
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'import_declaration') {
const source = node.childForFieldName('source')?.text.slice(1, -1) ?? '';
const specifiers = node.childForFieldName('specifiers');
specifiers?.children.forEach(spec => {
if (spec.type === 'import_specifier') {
const local = spec.childForFieldName('local')?.text ?? '';
const imported = spec.childForFieldName('imported')?.text ?? '';
fileImports.set(local, { source, imported });
}
});
}
node.children.forEach(visit);
};
visit(tree.rootNode);
this.imports.set(filePath, fileImports);
}
private collectDefinitions(filePath: string, tree: Parser.Tree): void {
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'function_declaration') {
const name = node.childForFieldName('name')?.text ?? '';
this.definitions.set(name, { file: filePath, node });
}
else if (node.type === 'variable_declarator') {
const name = node.childForFieldName('name')?.text;
const value = node.childForFieldName('value');
if (name && (value?.type === 'arrow_function' || value?.type === 'function')) {
this.definitions.set(name, { file: filePath, node: value });
}
}
node.children.forEach(visit);
};
visit(tree.rootNode);
}
private async getTypeFromPosition(uri: vscode.Uri, position: vscode.Position): Promise<string | null> {
const hover = await vscode.commands.executeCommand<vscode.Hover[]>(
'vscode.executeHoverProvider',
uri,
position
);
if (hover?.[0]?.contents.length) {
for (const content of hover[0].contents) {
let hoverText = typeof content === 'string' ?
content :
('value' in content ? content.value : '');
// Remove typescript backticks if present
hoverText = hoverText.replace(/```typescript\s*/, '').replace(/```\s*$/, '');
console.log('Processing hover text:', hoverText);
// Extract the type information - look for the type after the colon
const typeMatches = [
/:\s*([\w<>]+)(?:\[\])?/, // matches "foo: Type" or "foo: Type[]"
/var\s+\w+:\s*([\w<>]+)/, // matches "var foo: Type"
/\(type\)\s+[\w<>]+:\s*([\w<>]+)/, // matches "(type) foo: Type"
/\(method\)\s*([\w<>]+)\./ // matches "(method) Type.method"
];
for (const pattern of typeMatches) {
const match = pattern.exec(hoverText);
if (match) {
let type = match[1];
// Handle array types
if (hoverText.includes('[]')) {
return 'Array';
}
// Extract base type from generics
if (type.includes('<')) {
type = type.split('<')[0];
}
return type;
}
}
}
}
return null;
}
private async getCallsInDefn(defnNode: Parser.SyntaxNode, currentFile: string): Promise<Set<string>> {
const calls = new Set<string>();
const fileImports = this.imports.get(currentFile) ?? new Map();
const uri = vscode.Uri.file(currentFile);
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
if (node.type === 'call_expression') {
const callee = node.childForFieldName('function');
if (callee?.type === 'identifier') {
const name = callee.text;
const importInfo = fileImports.get(name);
if (importInfo) {
calls.add(`${importInfo.source}:${importInfo.imported}`);
} else {
calls.add(name);
}
}
else if (callee?.type === 'member_expression') {
const method = callee.childForFieldName('property')?.text;
const object = callee.childForFieldName('object');
if (method && object) {
const position = new vscode.Position(
object.startPosition.row,
object.startPosition.column
);
const type = await this.getTypeFromPosition(uri, position);
if (type) {
calls.add(`${type}.${method}`);
} else {
calls.add(`method:${method}`);
}
}
}
}
for (const child of node.children) {
await visit(child);
}
};
await visit(defnNode);
return calls;
}
private gotoDefn(name: string): Definition | null {
if (name.includes(':')) {
const [file, funcName] = name.split(':');
const def = this.definitions.get(funcName);
return def ?? null;
}
return this.definitions.get(name) ?? null;
}
private getUses(defnNode: Parser.SyntaxNode, currentFile: string): DefnUse[] {
const uses: DefnUse[] = [];
let fnName: string | undefined;
if (defnNode.type === 'function_declaration') {
fnName = defnNode.childForFieldName('name')?.text;
} else if (defnNode.type === 'arrow_function' || defnNode.type === 'function') {
const parent = defnNode.parent;
if (parent?.type === 'variable_declarator') {
fnName = parent.childForFieldName('name')?.text;
}
}
if (!fnName) return uses;
for (const [file, tree] of this.parsedFiles) {
const visit = (node: Parser.SyntaxNode): void => {
if (node.type === 'call_expression') {
const callee = node.childForFieldName('function');
if (callee?.type === 'identifier' && callee.text === fnName) {
let current: Parser.SyntaxNode | null = node;
while (current) {
if (current.type === 'function_declaration' ||
current.type === 'arrow_function' ||
current.type === 'function') {
uses.push({ parent: current, file });
break;
}
current = current.parent;
}
}
}
node.children.forEach(visit);
};
visit(tree.rootNode);
}
return uses;
}
private async visitAllNodesInGraphFromDefinition(defn: Parser.SyntaxNode, currentFile: string): Promise<void> {
let defnName: string | undefined;
if (defn.type === 'function_declaration') {
defnName = defn.childForFieldName('name')?.text;
} else if (defn.type === 'arrow_function' || defn.type === 'function') {
const parent = defn.parent;
if (parent?.type === 'variable_declarator') {
defnName = parent.childForFieldName('name')?.text;
}
}
if (!defnName) return;
const fullName = `${currentFile}:${defnName}`;
if (this.visited.has(fullName)) return;
const calls = await this.getCallsInDefn(defn, currentFile);
this.graph.set(fullName, calls);
this.visited.add(fullName);
const callDefns = Array.from(calls).map(call => this.gotoDefn(call));
for (const callDefn of callDefns) {
if (callDefn) {
await this.visitAllNodesInGraphFromDefinition(callDefn.node, callDefn.file);
}
}
const defnUses = this.getUses(defn, currentFile);
for (const defnUse of defnUses) {
await this.visitAllNodesInGraphFromDefinition(defnUse.parent, defnUse.file);
}
}
async analyze(entryFile: string): Promise<Map<string, Set<string>>> {
const tree = await this.parseFile(entryFile);
if (!tree) return new Map();
const visit = async (node: Parser.SyntaxNode): Promise<void> => {
if (node.type === 'function_declaration') {
await this.visitAllNodesInGraphFromDefinition(node, entryFile);
}
else if (node.type === 'variable_declarator') {
const value = node.childForFieldName('value');
if (value?.type === 'arrow_function' || value?.type === 'function') {
await this.visitAllNodesInGraphFromDefinition(value, entryFile);
}
}
for (const child of node.children) {
await visit(child);
}
};
await visit(tree.rootNode);
return this.graph;
}
}
export async function runTreeSitter(filePath?: string): Promise<Map<string, Set<string>> | null> {
const editor = vscode.window.activeTextEditor;
if (!editor && !filePath) {
vscode.window.showWarningMessage('No active editor found');
return null;
}
try {
const targetPath = filePath ?? editor!.document.uri.fsPath;
const analyzer = new ProjectAnalyzer();
const graph = await analyzer.analyze(targetPath);
for (const [defn, calls] of graph) {
console.log(`${defn} calls: ${[...calls].join(', ')}`);
}
return graph;
} catch (error) {
console.error('Error analyzing file:', error);
vscode.window.showErrorMessage('Error analyzing file');
return null;
}
}
@@ -0,0 +1,62 @@
import * as vscode from 'vscode';
const legend = new vscode.SemanticTokensLegend([], []);
export async function findFunctions() {
const editor = vscode.window.activeTextEditor;
if (!editor) return;
const document = editor.document;
const tokens = await vscode.commands.executeCommand<vscode.SemanticTokens>(
'vscode.provideDocumentSemanticTokens',
document.uri
);
if (!tokens) {
console.error('No tokens found');
return [];
}
const allTokens = decodeTokens(tokens, document);
return allTokens;
}
function decodeTokens(tokens: vscode.SemanticTokens, document: vscode.TextDocument) {
const data = tokens.data;
const decodedTokens = [];
let line = 0;
let character = 0;
for (let i = 0; i < data.length; i += 5) {
const deltaLine = data[i];
const deltaStartChar = data[i + 1];
const length = data[i + 2];
const tokenTypeIdx = data[i + 3];
const tokenModifierIdx = data[i + 4];
line += deltaLine;
character = deltaLine === 0 ? character + deltaStartChar : deltaStartChar;
const type = legend.tokenTypes[tokenTypeIdx] || `(${tokenTypeIdx})`;
const modifier = legend.tokenModifiers[tokenModifierIdx] || `(${tokenModifierIdx})`;
const tokenRange = new vscode.Range(line, character, line, character + length);
const tokenText = document.getText(tokenRange);
decodedTokens.push({
line,
startCharacter: character,
length,
type,
modifier,
text: tokenText,
});
console.log(`Token: '${tokenText}' | Type: ${type} | Modifier: ${modifier} | Line: ${line}, Character: ${character}`);
}
return decodedTokens;
}
-11
View File
@@ -1,11 +0,0 @@
Please see the `CONTRIBUTING.md` for information on how to contribute :)!
Here's an overview on how the extension works:
- The extension mounts in `extension.ts`.
- The Sidebar's HTML (everything in `sidebar/`) is built in React, and it's rendered by mounting a `<script>` tag - see `SidebarWebviewProvider.ts`.
- Communication between the sidebar script and the extension takes place via API. You can search for "postMessage" to see where API calls happen.
+3
View File
@@ -0,0 +1,3 @@
This folder is no longer relevant, since Void is no longer just an extension.
See the new Void code in `src/vs/workbench/contrib/void` (and a few other locations - you can just search "Void").
-19
View File
@@ -1,19 +0,0 @@
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const fs = require('fs')
const from = 'src/sidebar/styles.css'
const to = 'dist/sidebar/styles.css'
const original_css_contents = fs.readFileSync(from, 'utf8')
postcss([
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
autoprefixer,
])
.process(original_css_contents, { from, to })
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
.catch(error => {
console.error('Error in build-css:', error)
})
-13
View File
@@ -1,13 +0,0 @@
const esbuild = require('esbuild')
// Build JS
esbuild.build({
entryPoints: ['src/sidebar/index.tsx'],
bundle: true,
minify: true,
sourcemap: true,
outfile: 'dist/sidebar/index.js',
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
platform: 'browser',
external: ['vscode'],
}).catch(() => process.exit(1));
-8155
View File
File diff suppressed because it is too large Load Diff
-382
View File
@@ -1,382 +0,0 @@
{
"name": "void",
"publisher": "void",
"displayName": "Void",
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.89.0"
},
"categories": [
"Other"
],
"activationEvents": [],
"main": "./out/extension.js",
"contributes": {
"configuration": {
"title": "Void",
"properties": {
"void.whichApi": {
"type": "string",
"default": "anthropic",
"description": "Choose an API provider.",
"enum": [
"openAI",
"openAICompatible",
"anthropic",
"azure",
"ollama",
"greptile"
]
},
"void.anthropic.apiKey": {
"type": "string",
"default": "",
"description": "Anthropic API key."
},
"void.anthropic.model": {
"type": "string",
"default": "claude-3-5-sonnet-20240620",
"description": "Anthropic model to use.",
"enum": [
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307"
]
},
"void.anthropic.maxTokens": {
"type": "string",
"default": "8192",
"description": "Anthropic max number of tokens to output.",
"enum": [
"1024",
"2048",
"4096",
"8192"
]
},
"void.openAI.apiKey": {
"type": "string",
"default": "",
"description": "OpenAI API key."
},
"void.openAI.model": {
"type": "string",
"default": "gpt-4o",
"description": "OpenAI model to use.",
"enum": [
"o1-preview",
"o1-mini",
"gpt-4o",
"gpt-4o-2024-05-13",
"gpt-4o-2024-08-06",
"gpt-4o-mini",
"gpt-4o-mini-2024-07-18",
"gpt-4-turbo",
"gpt-4-turbo-2024-04-09",
"gpt-4-turbo-preview",
"gpt-4-0125-preview",
"gpt-4-1106-preview",
"gpt-4",
"gpt-4-0613",
"gpt-3.5-turbo-0125",
"gpt-3.5-turbo",
"gpt-3.5-turbo-1106"
]
},
"void.greptile.apiKey": {
"type": "string",
"default": "",
"description": "Greptile API key."
},
"void.greptile.githubPAT": {
"type": "string",
"default": "",
"description": "Github PAT given to Greptile to access your repository."
},
"void.greptile.remote": {
"type": "string",
"description": "remote provider",
"enum": [
"github",
"gitlab"
]
},
"void.greptile.repository": {
"type": "string",
"description": "Repository identifier in \"owner/repository\" format."
},
"void.greptile.branch": {
"type": "string",
"default": "main",
"description": "Name of the git branch."
},
"void.azure.apiKey": {
"type": "string",
"description": "Azure API key."
},
"void.azure.deploymentId": {
"type": "string",
"description": "Azure API deployment ID."
},
"void.azure.resourceName": {
"type": "string",
"description": "Name of the Azure OpenAI resource. Either this or `baseURL` can be used. \nThe resource name is used in the assembled URL: `https://{resourceName}.openai.azure.com/openai/deployments/{modelId}{path}`"
},
"void.azure.providerSettings": {
"type": "object",
"properties": {
"baseURL": {
"type": "string",
"default": "https://${resourceName}.openai.azure.com/openai/deployments",
"description": "Azure API base URL."
},
"headers": {
"type": "object",
"description": "Custom headers to include in the requests."
}
}
},
"void.ollama.endpoint": {
"type": "string",
"default": "http://127.0.0.1:11434",
"description": "The Ollama endpoint. Start Ollama by running `OLLAMA_ORIGINS=\"vscode-webview://*\" ollama serve`"
},
"void.ollama.model": {
"type": "string",
"default": "llama3.1",
"description": "Ollama model to use.",
"enum": [
"codegemma",
"codegemma:2b",
"codegemma:7b",
"codellama",
"codellama:7b",
"codellama:13b",
"codellama:34b",
"codellama:70b",
"codellama:code",
"codellama:python",
"command-r",
"command-r:35b",
"command-r-plus",
"command-r-plus:104b",
"deepseek-coder-v2",
"deepseek-coder-v2:16b",
"deepseek-coder-v2:236b",
"falcon2",
"falcon2:11b",
"firefunction-v2",
"firefunction-v2:70b",
"gemma",
"gemma:2b",
"gemma:7b",
"gemma2",
"gemma2:2b",
"gemma2:9b",
"gemma2:27b",
"llama2",
"llama2:7b",
"llama2:13b",
"llama2:70b",
"llama3",
"llama3:8b",
"llama3:70b",
"llama3-chatqa",
"llama3-chatqa:8b",
"llama3-chatqa:70b",
"llama3-gradient",
"llama3-gradient:8b",
"llama3-gradient:70b",
"llama3.1",
"llama3.1:8b",
"llama3.1:70b",
"llama3.1:405b",
"llava",
"llava:7b",
"llava:13b",
"llava:34b",
"llava-llama3",
"llava-llama3:8b",
"llava-phi3",
"llava-phi3:3.8b",
"mistral",
"mistral:7b",
"mistral-large",
"mistral-large:123b",
"mistral-nemo",
"mistral-nemo:12b",
"mixtral",
"mixtral:8x7b",
"mixtral:8x22b",
"moondream",
"moondream:1.8b",
"openhermes",
"openhermes:v2.5",
"phi3",
"phi3:3.8b",
"phi3:14b",
"phi3.5",
"phi3.5:3.8b",
"qwen",
"qwen:7b",
"qwen:14b",
"qwen:32b",
"qwen:72b",
"qwen:110b",
"qwen2",
"qwen2:0.5b",
"qwen2:1.5b",
"qwen2:7b",
"qwen2:72b",
"smollm",
"smollm:135m",
"smollm:360m",
"smollm:1.7b"
]
},
"void.openAICompatible.endpoint": {
"type": "string",
"default": "http://127.0.0.1:11434/v1",
"description": "The endpoint."
},
"void.openAICompatible.model": {
"type": "string",
"default": "gpt-4o",
"description": "The name of the model to use."
},
"void.openAICompatible.apiKey": {
"type": "string",
"default": "",
"description": "Your API key."
}
}
},
"commands": [
{
"command": "void.ctrl+l",
"title": "Show Sidebar"
},
{
"command": "void.ctrl+k",
"title": "Show Selection Lens"
},
{
"command": "void.acceptDiff",
"title": "Approve Diff"
},
{
"command": "void.rejectDiff",
"title": "Discard Diff"
},
{
"command": "void.startNewThread",
"title": "Start a new chat",
"icon": "$(add)"
},
{
"command": "void.toggleThreadSelector",
"title": "View past chats",
"icon": "$(history)"
}, {
"command": "void.openSettings",
"title": "Void settings",
"icon": "$(settings-gear)"
}
],
"viewsContainers": {
"activitybar": [
{
"id": "voidViewContainer",
"title": "Chat",
"icon": "$(hubot)"
}
]
},
"views": {
"voidViewContainer": [
{
"type": "webview",
"id": "void.viewnumberone",
"name": "Void"
}
]
},
"keybindings": [
{
"command": "void.ctrl+l",
"key": "ctrl+l",
"mac": "cmd+l"
},
{
"command": "void.ctrl+k",
"key": "ctrl+k",
"mac": "cmd+k"
}
],
"menus": {
"view/title": [
{
"command": "void.startNewThread",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.toggleThreadSelector",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
},
{
"command": "void.openSettings",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"watch": "tsc -watch -p ./",
"build": "rimraf dist && node build-tsx.js && node build-css.js",
"pretest": "tsc -p ./ && eslint src --ext ts",
"test": "vscode-test"
},
"devDependencies": {
"@eslint/js": "^9.9.1",
"@types/diff": "^5.2.2",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.12",
"@types/mocha": "^10.0.8",
"@types/node": "^22.5.1",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"@types/vscode": "1.89.0",
"@typescript-eslint/eslint-plugin": "^8.3.0",
"@typescript-eslint/parser": "^8.3.0",
"@vscode/test-cli": "^0.0.10",
"@vscode/test-electron": "2.4.1",
"autoprefixer": "^10.4.20",
"esbuild": "^0.23.1",
"eslint": "^8.57.0",
"eslint-plugin-react": "^7.35.1",
"eslint-plugin-react-hooks": "^4.6.2",
"globals": "^15.9.0",
"marked": "^14.1.0",
"postcss": "^8.4.41",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"rimraf": "^6.0.1",
"tailwindcss": "^3.4.10",
"typescript": "5.5.4",
"typescript-eslint": "^8.3.0"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.27.1",
"diff-match-patch": "^1.0.5",
"ollama": "^0.5.9",
"openai": "^4.57.0",
"diff": "^7.0.0"
}
}
@@ -1,315 +0,0 @@
import * as vscode from 'vscode';
import { findDiffs } from './findDiffs';
import { Diff, BaseDiffArea, BaseDiff, DiffArea } from './shared_types';
// TODO in theory this should be disposed
const greenDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(0 255 51 / 0.2)',
isWholeLine: false, // after: { contentText: ' [original]', color: 'rgba(0 255 60 / 0.5)' } // hoverMessage: originalText // this applies to hovering over after:...
})
// responsible for displaying diffs and showing accept/reject buttons
export class DisplayChangesProvider implements vscode.CodeLensProvider {
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
private _weAreEditing: boolean = false
// used internally by vscode
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
public readonly onDidChangeCodeLenses: vscode.Event<void> = this._onDidChangeCodeLenses.event;
// used internally by vscode
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
const docUriStr = document.uri.toString()
return this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses)
}
// declared by us, registered with vscode.languages.registerCodeLensProvider()
constructor() {
console.log('Creating DisplayChangesProvider')
// this acts as a useEffect. Every time text changes, clear the diffs in this editor
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
if (!editor)
return
if (this._weAreEditing)
return
const docUri = editor.document.uri
const docUriStr = docUri.toString()
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// loop through each change
for (const change of e.contentChanges) {
// here, `change.range` is the range of the original file that gets replaced with `change.text`
// compute net number of newlines lines that were added/removed
const numNewLines = (change.text.match(/\n/g) || []).length
const numLineDeletions = change.range.end.line - change.range.start.line
const deltaNewlines = numNewLines - numLineDeletions
// compute overlap with each diffArea and shrink/elongate the diffArea accordingly
for (const diffArea of diffAreas) {
// if the change is fully within the diffArea, elongate it by the delta amount of newlines
if (change.range.start.line >= diffArea.startLine && change.range.end.line <= diffArea.endLine) {
diffArea.endLine += deltaNewlines
}
// check if the `diffArea` was fully deleted and remove it if so
if (diffArea.startLine > diffArea.endLine) {
//remove it
const index = diffAreas.findIndex(da => da === diffArea)
diffAreas.splice(index, 1)
}
// TODO handle other cases where eg. the change overlaps many diffAreas
}
// if a diffArea is below the last character of the change, shift the diffArea up/down by the delta amount of newlines
for (const diffArea of diffAreas) {
if (diffArea.startLine > change.range.end.line) {
diffArea.startLine += deltaNewlines
diffArea.endLine += deltaNewlines
}
}
// TODO merge any diffAreas if they overlap with each other as a result from the shift
}
// refresh the diffAreas
this.refreshDiffAreas(docUri)
})
}
// used by us only
public addDiffArea(uri: vscode.Uri, diffArea: BaseDiffArea) {
const uriStr = uri.toString()
// make sure array is defined
if (!this._diffAreasOfDocument[uriStr])
this._diffAreasOfDocument[uriStr] = []
// remove all diffAreas that the new `diffArea` is overlapping with
this._diffAreasOfDocument[uriStr] = this._diffAreasOfDocument[uriStr].filter(da => {
const noOverlap = da.startLine > diffArea.endLine || da.endLine < diffArea.startLine
if (!noOverlap) return false
return true
})
// add `diffArea` to storage
this._diffAreasOfDocument[uriStr].push({
...diffArea,
diffareaid: this._diffareaidPool
})
this._diffareaidPool += 1
}
// used by us only
public refreshDiffAreas(docUri: vscode.Uri) {
const editor = vscode.window.activeTextEditor // TODO the editor should be that of `docUri` and not necessarily the current editor
if (!editor) {
console.log('Error: No active editor!')
return;
}
const docUriStr = docUri.toString()
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
// reset all diffs (we update them below)
this._diffsOfDocument[docUriStr] = []
// for each diffArea
for (const diffArea of diffAreas) {
// get code inside of diffArea
const currentCode = editor.document.getText(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)).replace(/\r\n/g, '\n')
// compute the diffs
const diffs = findDiffs(diffArea.originalCode, currentCode)
// print diffs
console.log('!CODEBefore:', JSON.stringify(diffArea.originalCode))
console.log('!CODEAfter:', JSON.stringify(currentCode))
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.addDiffs(editor.document.uri, diffs, diffArea)
for (const diff of this._diffsOfDocument[docUriStr]) {
console.log('------------')
console.log('deletedCode:', JSON.stringify(diff.deletedCode))
console.log('insertedCode:', JSON.stringify(diff.insertedCode))
console.log('deletedRange:', diff.deletedRange.start.line, diff.deletedRange.end.line,)
console.log('insertedRange:', diff.insertedRange.start.line, diff.insertedRange.end.line,)
}
}
// update green highlighting
editor.setDecorations(
greenDecoration,
(this._diffsOfDocument[docUriStr]
.filter(diff => diff.insertedRange !== undefined)
.map(diff => diff.insertedRange)
)
);
// TODO update red highlighting
// this._diffsOfDocument[docUriStr].map(diff => diff.deletedCode)
// update code lenses
this._onDidChangeCodeLenses.fire()
}
// used by us only
public addDiffs(docUri: vscode.Uri, diffs: BaseDiff[], diffArea: DiffArea) {
const docUriStr = docUri.toString()
// if no diffs, set diffs to []
if (!this._diffsOfDocument[docUriStr])
this._diffsOfDocument[docUriStr] = []
// add each diff and its codelens to the document
for (let i = diffs.length - 1; i > -1; i -= 1) {
let suggestedDiff = diffs[i]
this._diffsOfDocument[docUriStr].push({
...suggestedDiff,
diffid: this._diffidPool,
// originalCode: suggestedDiff.deletedText,
lenses: [
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.insertedRange, { title: 'Reject', command: 'void.rejectDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] })
]
});
this._diffidPool += 1
}
}
// called on void.acceptDiff
public async acceptDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// get document uri
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get relevant diff
// TODO speed up with hashmap
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) {
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
// get relevant diffArea
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) {
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// replace `originalCode[diff.deletedRange]` with diff.insertedCode
// TODO add a history event to undo this change
const originalLines = diffArea.originalCode.split('\n');
const relativeStart = diff.deletedRange.start.line - diffArea.originalStartLine
const relativeEnd = diff.deletedRange.end.line - diffArea.originalStartLine
diffArea.originalCode = [
...originalLines.slice(0, relativeStart), // lines before the deleted range
...diff.insertedCode.split('\n'), // inserted lines
...originalLines.slice(relativeEnd + 1) // lines after the deleted range
].join('\n')
// if the diffArea has no changes, remove it
const currentDiffAreaCode = editor.document.getText()
.replace(/\r\n/g, '\n')
.split('\n')
.slice(diffArea.startLine, diffArea.endLine + 1)
.join('\n')
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
// refresh the diff area
this.refreshDiffAreas(docUri)
}
// called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// get document uri
const docUri = editor.document.uri
const docUriStr = docUri.toString()
// get relevant diff
// TODO speed up with hashmap
const diffIdx = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
if (diffIdx === -1) {
console.error('Error: DiffID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
// get relevant diffArea
const diffareaIdx = this._diffAreasOfDocument[docUriStr].findIndex(diff => diff.diffareaid === diffareaid);
if (diffareaIdx === -1) {
console.error('Error: DiffAreaID could not be found: ', diffid, diffareaid, this._diffsOfDocument[docUriStr], this._diffAreasOfDocument[docUriStr]); return;
}
const diff = this._diffsOfDocument[docUriStr][diffIdx]
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
// replace `editorCode[diff.insertedRange]` with diff.deletedCode
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(docUri, diff.insertedRange, diff.deletedCode)
this._weAreEditing = true
await vscode.workspace.applyEdit(workspaceEdit)
this._weAreEditing = false
// if the diffArea has no changes, remove it
const currentDiffAreaCode = editor.document.getText()
.replace(/\r\n/g, '\n')
.split('\n')
.slice(diffArea.startLine, diffArea.endLine + 1)
.join('\n')
if (diffArea.originalCode === currentDiffAreaCode) { // if the currentDiffAreaCode === diffArea.originalCode, remove the diffArea
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
// refresh the diff area
this.refreshDiffAreas(docUri)
}
}
@@ -1,105 +0,0 @@
// renders the code from `src/sidebar`
import * as vscode from 'vscode';
function getNonce() {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
public static readonly viewId = 'void.viewnumberone';
public webview: Promise<vscode.Webview> // used to send messages to the webview, resolved by _res in resolveWebviewView
private _res: (c: vscode.Webview) => void // used to resolve the webview
private readonly _extensionUri: vscode.Uri
private _webviewView?: vscode.WebviewView; // only used inside onDidChangeConfiguration
private _webviewDeps: string[] = [];
constructor(context: vscode.ExtensionContext) {
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later... was included in webviewProvider code
this._extensionUri = context.extensionUri
let temp_res: typeof this._res | undefined = undefined
this.webview = new Promise((res, rej) => { temp_res = res })
if (!temp_res) throw new Error("sidebar provider: resolver was undefined")
this._res = temp_res
// if it affects one of the config items webview depends on, update the webview
// TODO should be able to move this entirely to React - make updateWebviewHTML mount once, and then send updates via postMessage from then on
vscode.workspace.onDidChangeConfiguration(event => {
if (this._webviewDeps.map(dep => event.affectsConfiguration(dep)).some(v => !!v)) {
if (this._webviewView) {
this.updateWebviewHTML(this._webviewView.webview);
}
}
});
}
// this is updated
private updateWebviewHTML(webview: vscode.Webview) {
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com'];
this._webviewDeps = []
const ollamaEndpoint: string | undefined = vscode.workspace.getConfiguration('void.ollama').get('endpoint');
this._webviewDeps.push('void.ollama.endpoint');
if (ollamaEndpoint)
allowed_urls.push(ollamaEndpoint);
const openAICompatibleEndpoint: string | undefined = vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint');
this._webviewDeps.push('void.openAICompatible.endpoint');
if (openAICompatibleEndpoint)
allowed_urls.push(openAICompatibleEndpoint);
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/index.js'));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri, 'dist/sidebar/styles.css'));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(this._extensionUri));
const nonce = getNonce();
const webviewHTML = `<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8">
<meta name="viewport" content="width=device-width, initial-scale=1.0">
<title>Custom View</title>
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}'; style-src vscode-resource: 'unsafe-inline' http: https: data:;">
<base href="${rootUri}/">
<link href="${stylesUri}" rel="stylesheet">
</head>
<body>
<div id="root"></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
webview.html = webviewHTML;
}
// called internally by vscode
resolveWebviewView(
webviewView: vscode.WebviewView,
context: vscode.WebviewViewResolveContext,
token: vscode.CancellationToken,
) {
const webview = webviewView.webview;
webview.options = {
enableScripts: true,
localResourceRoots: [this._extensionUri]
};
this.updateWebviewHTML(webview);
// resolve webview and _webviewView
this._res(webview);
this._webviewView = webviewView;
}
}
@@ -1,300 +0,0 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
// always compare these against package.json to make sure every setting in this type can actually be provided by the user
export type ApiConfig = {
anthropic: {
apikey: string,
model: string,
maxTokens: string
},
openAI: {
apikey: string,
model: string,
},
greptile: {
apikey: string,
githubPAT: string,
repoinfo: {
remote: string, // e.g. 'github'
repository: string, // e.g. 'voideditor/void'
branch: string // e.g. 'main'
}
},
ollama: {
endpoint: string,
model: string
},
openAICompatible: {
endpoint: string,
model: string,
apikey: string
}
whichApi: string
}
type OnText = (newText: string, fullText: string) => void
export type LLMMessage = {
role: 'user' | 'assistant',
content: string
}
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (input: string) => void,
apiConfig: ApiConfig,
})
=> {
abort: () => void
}
type SendLLMMessageFnTypeExternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (input: string) => void,
apiConfig: ApiConfig | null,
})
=> {
abort: () => void
}
// Claude
const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
const stream = anthropic.messages.stream({
model: apiConfig.anthropic.model,
max_tokens: parseInt(apiConfig.anthropic.maxTokens),
messages: messages,
});
let did_abort = false
// when receive text
stream.on('text', (newText, fullText) => {
if (did_abort) return
onText(newText, fullText)
})
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (claude_response) => {
if (did_abort) return
// stringify the response's content
let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n');
onFinalMessage(content)
})
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
const abort = () => {
// stream.abort() // this doesnt appear to do anything, but it should try to stop claude from generating anymore
did_abort = true
}
return { abort }
};
// OpenAI and OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => {
didAbort = true;
};
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
if (apiConfig.whichApi === 'openAI') {
openai = new OpenAI({ baseURL: apiConfig.openAICompatible.endpoint, apiKey: apiConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
options = { model: apiConfig.openAI.model, messages: messages, stream: true, }
}
else if (apiConfig.whichApi === 'openAICompatible') {
openai = new OpenAI({ apiKey: apiConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: apiConfig.openAICompatible.model, messages: messages, stream: true, }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${apiConfig.whichApi}`)
throw new Error(`apiConfig.whichAPI was invalid: ${apiConfig.whichApi}`)
}
openai.chat.completions
.create(options)
.then(async response => {
abort = () => {
// response.controller.abort()
didAbort = true;
}
// when receive text
try {
for await (const chunk of response) {
if (didAbort) return;
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
}
// when error/fail
catch (error) {
console.error('Error in OpenAI stream:', error);
onFinalMessage(fullText);
}
})
return { abort };
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let didAbort = false
let fullText = ""
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort = () => {
didAbort = true;
};
const ollama = new Ollama({ host: apiConfig.ollama.endpoint })
ollama.chat({
model: apiConfig.ollama.model,
messages: messages,
stream: true,
})
.then(async stream => {
abort = () => {
// ollama.abort()
didAbort = true
}
// iterate through the stream
try {
for await (const chunk of stream) {
if (didAbort) return;
const newText = chunk.message.content;
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
}
// when error/fail
catch (error) {
console.error('Error:', error);
onFinalMessage(fullText);
}
})
return { abort };
};
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
let abort: () => void = () => { didAbort = true }
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
"Authorization": `Bearer ${apiConfig.greptile.apikey}`,
"X-Github-Token": `${apiConfig.greptile.githubPAT}`,
"Content-Type": `application/json`,
},
body: JSON.stringify({
messages,
stream: true,
repositories: [apiConfig.greptile.repoinfo]
}),
})
// this is {message}\n{message}\n{message}...\n
.then(async response => {
const text = await response.text()
console.log('got greptile', text)
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
})
// TODO make this actually stream, right now it just sends one message at the end
.then(async responseArr => {
if (didAbort)
return
for (let response of responseArr) {
const type: string = response['type']
const message = response['message']
// when receive text
if (type === 'message') {
fullText += message
onText(message, fullText)
}
else if (type === 'sources') {
const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null }
fullText += filepath
onText(filepath, fullText)
}
// type: 'status' with an empty 'message' means last message
else if (type === 'status') {
if (!message) {
onFinalMessage(fullText)
}
}
}
})
.catch(e => {
console.error('Error in Greptile stream:', e);
onFinalMessage(fullText);
});
return { abort }
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
if (!apiConfig) return { abort: () => { } }
switch (apiConfig.whichApi) {
case 'anthropic':
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig });
case 'openAI':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig });
case 'greptile':
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, apiConfig });
default:
console.error(`Error: whichApi was ${apiConfig.whichApi}, which is not recognized!`);
return { abort: () => { } }
//return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }); // TODO
}
}
-203
View File
@@ -1,203 +0,0 @@
import * as vscode from 'vscode';
import { DisplayChangesProvider } from './DisplayChangesProvider';
import { BaseDiffArea, ChatThreads, WebviewMessage } from './shared_types';
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
import { ApiConfig } from './common/sendLLMMessage';
const readFileContentOfUri = async (uri: vscode.Uri) => {
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8')
.replace(/\r\n/g, '\n') // replace windows \r\n with \n
}
const getApiConfig = () => {
const apiConfig: ApiConfig = {
anthropic: {
apikey: vscode.workspace.getConfiguration('void.anthropic').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.anthropic').get('model') ?? '',
maxTokens: vscode.workspace.getConfiguration('void.anthropic').get('maxTokens') ?? '',
},
openAI: {
apikey: vscode.workspace.getConfiguration('void.openAI').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.openAI').get('model') ?? '',
},
greptile: {
apikey: vscode.workspace.getConfiguration('void.greptile').get('apiKey') ?? '',
githubPAT: vscode.workspace.getConfiguration('void.greptile').get('githubPAT') ?? '',
repoinfo: {
remote: 'github',
repository: 'TODO',
branch: 'main'
}
},
ollama: {
endpoint: vscode.workspace.getConfiguration('void.ollama').get('endpoint') ?? '',
model: vscode.workspace.getConfiguration('void.ollama').get('model') ?? '',
},
openAICompatible: {
endpoint: vscode.workspace.getConfiguration('void.openAICompatible').get('endpoint') ?? '',
apikey: vscode.workspace.getConfiguration('void.openAICompatible').get('apiKey') ?? '',
model: vscode.workspace.getConfiguration('void.openAICompatible').get('model') ?? '',
},
whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? ''
}
return apiConfig
}
export function activate(context: vscode.ExtensionContext) {
// 1. Mount the chat sidebar
const webviewProvider = new SidebarWebviewProvider(context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, webviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
);
// 2. Activate the sidebar on ctrl+l
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+l', () => {
const editor = vscode.window.activeTextEditor
if (!editor)
return
// show the sidebar
vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
// vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
// get the text the user is selecting
const selectionStr = editor.document.getText(editor.selection);
// get the range of the selection
const selectionRange = editor.selection;
// get the file the user is in
const filePath = editor.document.uri;
// send message to the webview (Sidebar.tsx)
webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies WebviewMessage));
})
);
// 3. Show an approve/reject codelens above each change
const displayChangesProvider = new DisplayChangesProvider();
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', displayChangesProvider));
// 4. Add approve/reject commands
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
displayChangesProvider.acceptDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
displayChangesProvider.rejectDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.openSettings', async () => {
vscode.commands.executeCommand('workbench.action.openSettings', '@ext:void.void');
}));
// 5. Receive messages from sidebar
webviewProvider.webview.then(
webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies WebviewMessage)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies WebviewMessage)
}))
// when config changes, send it to the sidebar
vscode.workspace.onDidChangeConfiguration(e => {
if (e.affectsConfiguration('void')) {
const apiConfig = getApiConfig()
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
}
})
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: WebviewMessage) => {
if (m.type === 'requestFiles') {
// get contents of all file paths
const files = await Promise.all(
m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) }))
)
// send contents to webview
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
} else if (m.type === 'applyChanges') {
const editor = vscode.window.activeTextEditor
if (!editor) {
vscode.window.showInformationMessage('No active editor!')
return
}
// create an area to show diffs
const diffArea: BaseDiffArea = {
startLine: 0, // in ctrl+L the start and end lines are the full document
endLine: editor.document.lineCount,
originalStartLine: 0,
originalEndLine: editor.document.lineCount,
originalCode: await readFileContentOfUri(editor.document.uri),
}
displayChangesProvider.addDiffArea(editor.document.uri, diffArea)
// write new code `m.code` to the document
// TODO update like this:
// this._weAreEditing = true
// await vscode.workspace.applyEdit(workspaceEdit)
// await vscode.workspace.save(docUri)
// this._weAreEditing = false
await editor.edit(editBuilder => {
editBuilder.replace(new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER), m.code);
});
// rediff the changes based on the diffAreas
displayChangesProvider.refreshDiffAreas(editor.document.uri)
}
else if (m.type === 'getApiConfig') {
const apiConfig = getApiConfig()
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
}
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies WebviewMessage)
}
else if (m.type === 'persistThread') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
const updatedThreads: ChatThreads = { ...threads, [m.thread.id]: m.thread }
context.workspaceState.update('allThreads', updatedThreads)
}
else {
console.error('unrecognized command', m.type, m)
}
})
}
)
// Gets called when user presses ctrl + k (mounts ctrl+k-style codelens)
// TODO need to build this
// const ctrlKCodeLensProvider = new CtrlKCodeLensProvider();
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider));
// context.subscriptions.push(
// vscode.commands.registerCommand('void.ctrl+k', () => {
// const editor = vscode.window.activeTextEditor;
// if (!editor)
// return
// ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection);
// // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses
// })
// )
}
-277
View File
@@ -1,277 +0,0 @@
import * as vscode from 'vscode';
// import { diffLines, Change } from 'diff';
import { BaseDiff } from './shared_types';
import { diff_match_patch } from 'diff-match-patch';
const diffLines = (text1: string, text2: string) => {
var dmp = new diff_match_patch();
var a = dmp.diff_linesToChars_(text1, text2);
var lineText1 = a.chars1;
var lineText2 = a.chars2;
var lineArray = a.lineArray;
var diffs = dmp.diff_main(lineText1, lineText2, false);
dmp.diff_charsToLines_(diffs, lineArray);
// dmp.diff_cleanupSemantic(diffs);
return diffs;
}
// TODO use a better diff algorithm
export const findDiffs = (oldText: string, newText: string): BaseDiff[] => {
const diffs = diffLines(oldText, newText);
const blocks: BaseDiff[] = [];
let reprBlock: string[] = [];
let deletedBlock: string[] = [];
let insertedBlock: string[] = [];
let insertedLine = 0;
let deletedLine = 0;
let insertedStart = 0;
let deletedStart = 0;
diffs.forEach(([operation, text]) => {
const lines = text.split('\n');
switch (operation) {
// insertion
case 1:
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
if (insertedBlock.length === 0) insertedStart = insertedLine;
insertedLine += lines.length - 1; // Update only the line count for new text
insertedBlock.push(text);
reprBlock.push(lines.map(line => `+ ${line}`).join('\n'));
break;
// deletion
case -1:
if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
if (deletedBlock.length === 0) deletedStart = deletedLine;
deletedLine += lines.length - 1; // Update only the line count for old text
deletedBlock.push(text);
reprBlock.push(lines.map(line => `- ${line}`).join('\n'));
break;
// no change
case 0:
// If we have a pending block, add it to the blocks array
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
blocks.push({
code: reprBlock.join(''),
deletedCode: deletedBlock.join(''),
insertedCode: insertedBlock.join(''),
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
});
}
// Reset the block variables
reprBlock = [];
deletedBlock = [];
insertedBlock = [];
// Update line counts for unchanged text
insertedLine += lines.length - 1;
deletedLine += lines.length - 1;
break;
}
});
// Add any remaining blocks after the loop ends
if (insertedBlock.length > 0 || deletedBlock.length > 0) {
blocks.push({
code: reprBlock.join(''),
deletedCode: deletedBlock.join(''),
insertedCode: insertedBlock.join(''),
deletedRange: new vscode.Range(deletedStart, 0, deletedLine, Number.MAX_SAFE_INTEGER),
insertedRange: new vscode.Range(insertedStart, 0, insertedLine, Number.MAX_SAFE_INTEGER),
});
}
return blocks;
};
// export const findDiffs = (oldText: string, newText: string): DiffBlock[] => {
// const diffs = diffLines(oldText, newText);
// const blocks: DiffBlock[] = [];
// let reprBlock: string[] = [];
// let deletedBlock: string[] = [];
// let insertedBlock: string[] = [];
// let insertedEnd = 0;
// let deletedEnd = 0;
// let insertedStart = 0;
// let deletedStart = 0;
// diffs.forEach(part => {
// part.count = part.count ?? 0
// // if the part is an addition or deletion, add it to the current block
// if (part.added || part.removed) {
// if (reprBlock.length === 0) { reprBlock.push('@@@@'); }
// if (part.added) {
// if (insertedBlock.length === 0) insertedStart = insertedEnd;
// insertedEnd += part.count
// insertedBlock.push(part.value);
// reprBlock.push(part.value.split('\n').map(line => `+ ${line}`).join('\n'));
// }
// if (part.removed) {
// if (deletedBlock.length === 0) deletedStart = deletedEnd;
// deletedEnd += part.count
// deletedBlock.push(part.value);
// reprBlock.push(part.value.split('\n').map(line => `- ${line}`).join('\n'));
// }
// }
// // if the part is unchanged, finalize the block and add it to the array
// else {
// // if the block is not null, add it to the array
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
// blocks.push({
// code: reprBlock.join('\n'),
// deletedCode: deletedBlock.join(''),
// insertedCode: insertedBlock.join(''),
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
// });
// }
// // update block variables
// reprBlock = [];
// deletedBlock = [];
// insertedBlock = [];
// insertedEnd += part.count;
// deletedEnd += part.count;
// }
// })
// // finally, add the last block to the array
// if (insertedBlock.length > 0 || deletedBlock.length > 0) {
// blocks.push({
// code: reprBlock.join('\n'),
// deletedCode: deletedBlock.join(''),
// insertedCode: insertedBlock.join(''),
// deletedRange: new vscode.Range(deletedStart, 0, deletedEnd, Number.MAX_SAFE_INTEGER),
// insertedRange: new vscode.Range(insertedStart, 0, insertedEnd, Number.MAX_SAFE_INTEGER),
// });
// }
// return blocks;
// }
// import { diffLines, Change } from 'diff';
// export type SuggestedEdit = {
// // start/end of current file
// startLine: number;
// endLine: number;
// // start/end of original file
// originalStartLine: number,
// originalEndLine: number,
// // original content (originalfile[originalStart...originalEnd])
// originalContent: string;
// newContent: string;
// }
// export function getDiffedLines(oldStr: string, newStr: string) {
// // an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
// const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
// console.debug('Line by line changes', lineByLineChanges)
// lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
// let oldFileLineNum: number = 0;
// let newFileLineNum: number = 0;
// let streakStartInNewFile: number | undefined = undefined
// let streakStartInOldFile: number | undefined = undefined
// let oldStrLines = oldStr.split('\n')
// let newStrLines = newStr.split('\n')
// const replacements: SuggestedEdit[] = []
// for (let line of lineByLineChanges) {
// // no change on this line
// if (!line.added && !line.removed) {
// // if we were on a streak, add it
// if (streakStartInNewFile !== undefined) {
// const startLine = streakStartInNewFile
// const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
// const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
// const originalStartLine = streakStartInOldFile!
// const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
// const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
// const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent }
// replacements.push(replacement)
// streakStartInNewFile = undefined
// streakStartInOldFile = undefined
// }
// oldFileLineNum += line.count ?? 0;
// newFileLineNum += line.count ?? 0;
// }
// // line was removed from old file
// else if (line.removed) {
// // if we weren't on a streak, start one on this current line num
// if (streakStartInNewFile === undefined) {
// streakStartInNewFile = newFileLineNum
// streakStartInOldFile = oldFileLineNum
// }
// oldFileLineNum += line.count ?? 0 // we processed the line so add 1
// }
// // line was added to new file
// else if (line.added) {
// // if we weren't on a streak, start one on this current line num
// if (streakStartInNewFile === undefined) {
// streakStartInNewFile = newFileLineNum
// streakStartInOldFile = oldFileLineNum
// }
// newFileLineNum += line.count ?? 0; // we processed the line so add 1
// }
// } // end for
// console.debug('Replacements', replacements)
// return replacements
// }
-112
View File
@@ -1,112 +0,0 @@
import * as vscode from 'vscode';
import { ApiConfig } from './common/sendLLMMessage';
// a selection is a frozen snapshot
type CodeSelection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
type File = { filepath: vscode.Uri, content: string }
// an area that is currently being diffed
type BaseDiffArea = {
// use `startLine` and `endLine` instead of `range` for mutibility
// bounds are relative to the file, inclusive
startLine: number;
endLine: number;
originalStartLine: number,
originalEndLine: number,
originalCode: string, // the original chunk of code (not necessarily the whole file)
// `newCode: string,` is not included because it is the code in the actual file, `document.text()[startline: endLine + 1]`
}
type DiffArea = BaseDiffArea & { diffareaid: number }
// the return type of diff creator
type BaseDiff = {
code: string; // representation of the diff in text
deletedRange: vscode.Range; // relative to the file, inclusive
insertedRange: vscode.Range;
deletedCode: string;
insertedCode: string;
}
// each diff on the user's screen
type Diff = {
diffid: number,
lenses: vscode.CodeLens[],
} & BaseDiff
type WebviewMessage = (
// editor -> sidebar
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor
// sidebar -> editor
| { type: 'applyChanges', code: string } // user clicks "apply" in the sidebar
// sidebar -> editor
| { type: 'requestFiles', filepaths: vscode.Uri[] }
// editor -> sidebar
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
// sidebar -> editor
| { type: 'getApiConfig' }
// editor -> sidebar
| { type: 'apiConfig', apiConfig: ApiConfig }
// sidebar -> editor
| { type: 'getAllThreads' }
// editor -> sidebar
| { type: 'allThreads', threads: ChatThreads }
// sidebar -> editor
| { type: 'persistThread', thread: ChatThreads[string] }
// editor -> sidebar
| { type: 'startNewThread' }
// editor -> sidebar
| { type: 'toggleThreadSelector' }
)
type Command = WebviewMessage['type']
type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string;
messages: ChatMessage[];
}
}
type ChatMessage =
| {
role: "user";
content: string; // content sent to the llm
displayContent: string; // content displayed to user
selection: CodeSelection | null; // the user's selection
files: vscode.Uri[]; // the files sent in the message
}
| {
role: "assistant";
content: string; // content received from LLM
displayContent: string; // content displayed to user (this is the same as content for now)
}
export {
BaseDiff, BaseDiffArea,
Diff, DiffArea,
CodeSelection,
File,
WebviewMessage,
Command,
ChatThreads,
ChatMessage,
}
-321
View File
@@ -1,321 +0,0 @@
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
import { ApiConfig, sendLLMMessage } from "../common/sendLLMMessage"
import { File, CodeSelection, WebviewMessage, ChatMessage } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
import { marked } from 'marked';
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import * as vscode from 'vscode'
import { SelectedFiles } from "./components/SelectedFiles";
import { useThreads } from "./threadsContext";
const filesStr = (fullFiles: File[]) => {
return fullFiles.map(({ filepath, content }) =>
`
${filepath.fsPath}
\`\`\`
${content}
\`\`\``).join('\n')
}
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
return `
${filesStr(files)}
${!selection ? '' : `
I am currently selecting this code:
\`\`\`${selection.selectionStr}\`\`\`
`}
Please edit the code following these instructions (or, if appropriate, answer my question instead):
${instructions}
If you make a change, rewrite the entire file.
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
}
const ChatBubble = ({ chatMessage }: { chatMessage: ChatMessage }) => {
const role = chatMessage.role
const children = chatMessage.displayContent
if (!children)
return null
let chatbubbleContents: React.ReactNode
if (role === 'user') {
chatbubbleContents = <>
<SelectedFiles files={chatMessage.files} setFiles={null} />
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} hideToolbar />}
{children}
</>
}
else if (role === 'assistant') {
chatbubbleContents = <MarkdownRender string={children} /> // sectionsHTML
}
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
{chatbubbleContents}
</div>
</div>
}
const ThreadSelector = ({ onClose }: { onClose: () => void }) => {
const { allThreads, currentThread, switchToThread } = useThreads()
return (
<div className="flex flex-col space-y-1">
<div className="text-right">
<button className="btn btn-sm" onClick={onClose}>
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</button>
</div>
{/* iterate through all past threads */}
{Object.keys(allThreads ?? {}).map((threadId) => {
const pastThread = (allThreads ?? {})[threadId];
return (
<button
key={pastThread.id}
className={`btn btn-sm btn-secondary ${pastThread.id === currentThread?.id ? "btn-primary" : ""}`}
onClick={() => switchToThread(pastThread.id)}
>
{new Date(pastThread.createdAt).toLocaleString()}
</button>
)
})}
</div>
)
}
const Sidebar = () => {
const { allThreads, currentThread, addMessageToHistory, startNewThread, } = useThreads()
// state of current message
const [selection, setSelection] = useState<CodeSelection | null>(null) // the code the user is selecting
const [files, setFiles] = useState<vscode.Uri[]>([]) // the names of the files in the chat
const [instructions, setInstructions] = useState('') // the user's instructions
// state of chat
const [messageStream, setMessageStream] = useState('')
const [isLoading, setIsLoading] = useState(false)
const [isThreadSelectorOpen, setIsThreadSelectorOpen] = useState(false)
const abortFnRef = useRef<(() => void) | null>(null)
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(null)
// get Api Config on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: 'getApiConfig' })
}, [])
// Receive messages from the extension
useEffect(() => {
const listener = (event: MessageEvent) => {
const m = event.data as WebviewMessage;
// resolve any awaiting promises
// eg. it will resolve the promise below for `await VSCodeResponse('files')`
resolveAwaitingVSCodeResponse(m)
// if user pressed ctrl+l, add their selection to the sidebar
if (m.type === 'ctrl+l') {
setSelection(m.selection)
const filepath = m.selection.filePath
// add current file to the context if it's not already in the files array
if (!files.find(f => f.fsPath === filepath.fsPath))
setFiles(files => [...files, filepath])
}
// when get apiConfig, set
else if (m.type === 'apiConfig') {
setApiConfig(m.apiConfig)
}
// if they pressed the + to add a new chat
else if (m.type === 'startNewThread') {
setIsThreadSelectorOpen(false)
if (currentThread?.messages.length !== 0)
startNewThread()
}
// if they opened thread selector
else if (m.type === 'toggleThreadSelector') {
setIsThreadSelectorOpen(v => !v)
}
}
window.addEventListener('message', listener);
return () => { window.removeEventListener('message', listener) }
}, [files, selection, startNewThread, currentThread])
const formRef = useRef<HTMLFormElement | null>(null)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isLoading) return
setIsLoading(true)
setInstructions('');
formRef.current?.reset(); // reset the form's text
setSelection(null)
setFiles([])
// request file content from vscode and await response
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files')
// add message to chat history
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
// console.log('prompt:\n', content)
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
addMessageToHistory(newHistoryElt)
// send message to LLM
let { abort } = sendLLMMessage({
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => {
// add assistant's message to chat history, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
addMessageToHistory(newHistoryElt)
// clear selection
setMessageStream('')
setIsLoading(false)
},
apiConfig: apiConfig
})
abortFnRef.current = abort
}
const onStop = useCallback(() => {
// abort claude
abortFnRef.current?.()
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(canceled)'
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
}, [addMessageToHistory, messageStream])
//Clear code selection
const clearSelection = () => {
setSelection(null);
};
return <>
<div className="flex flex-col h-screen w-full">
{isThreadSelectorOpen && (
<div className="mb-2 max-h-[30vh] overflow-y-auto">
<ThreadSelector onClose={() => setIsThreadSelectorOpen(false)} />
</div>
)}
<div className="overflow-y-auto overflow-x-hidden space-y-4">
{/* previous messages */}
{currentThread !== null && currentThread.messages.map((message, i) =>
<ChatBubble key={i} chatMessage={message} />
)}
{/* message stream */}
<ChatBubble chatMessage={{ role: 'assistant', content: messageStream, displayContent: messageStream }} />
</div>
{/* chatbar */}
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
<div className="relative">
<div className="input">
{/* selection */}
{(files.length || selection?.selectionStr) && <div className="p-2 pb-0 space-y-2">
{/* selected files */}
<SelectedFiles files={files} setFiles={setFiles} />
{/* selected code */}
{!!selection?.selectionStr && (
<BlockCode className="rounded bg-vscode-sidebar-bg" text={selection.selectionStr} toolbar={(
<button
onClick={clearSelection}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>
Remove
</button>
)} />
)}
</div>}
<form
ref={formRef}
className="flex flex-row items-center rounded-md p-2"
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
onSubmit={(e) => {
console.log('submit!')
e.preventDefault();
onSubmit(e)
}}>
{/* input */}
<textarea
onChange={(e) => { setInstructions(e.target.value) }}
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
placeholder="Ctrl+L to select"
rows={1}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
/>
{/* submit button */}
{isLoading ?
<button
onClick={onStop}
className="btn btn-primary rounded-r-lg max-h-10 p-2"
type='button'
>Stop</button>
: <button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={!instructions}
type='submit'
>
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
<line x1="12" y1="19" x2="12" y2="5"></line>
<polyline points="5 12 12 5 19 12"></polyline>
</svg>
</button>
}
</form>
</div>
</div>
</div>
</div>
</div>
</>
}
export default Sidebar
@@ -1,48 +0,0 @@
import React from "react"
import * as vscode from "vscode"
const getBasename = (pathStr: string) => {
// "unixify" path
pathStr = pathStr.replace(/[/\\]+/g, "/") // replace any / or \ or \\ with /
const parts = pathStr.split("/") // split on /
return parts[parts.length - 1]
}
export const SelectedFiles = ({ files, setFiles, }: { files: vscode.Uri[], setFiles: null | ((files: vscode.Uri[]) => void) }) => {
return (
files.length !== 0 && (
<div className="flex flex-wrap -mx-1 -mb-1">
{files.map((filename, i) => (
<button
key={filename.path}
disabled={!setFiles}
className={`btn btn-secondary btn-sm border border-vscode-input-border rounded flex items-center space-x-2 mx-1 mb-1 disabled:cursor-default`}
type="button"
onClick={() => setFiles?.([...files.slice(0, i), ...files.slice(i + 1, Infinity)])}
>
<span>{getBasename(filename.fsPath)}</span>
{/* X button */}
{!!setFiles && <span className="">
<svg
xmlns="http://www.w3.org/2000/svg"
fill="none"
viewBox="0 0 24 24"
stroke="currentColor"
className="size-4"
>
<path
strokeLinecap="round"
strokeLinejoin="round"
d="M6 18 18 6M6 6l12 12"
/>
</svg>
</span>}
</button>
))}
</div>
)
)
}
@@ -1,59 +0,0 @@
import { Command, WebviewMessage } from "../shared_types";
// message -> res[]
const awaiting: { [c in Command]: ((res: any) => void)[] } = {
"ctrl+l": [],
"applyChanges": [],
"requestFiles": [],
"files": [],
"apiConfig": [],
"getApiConfig": [],
"startNewThread": [],
"getAllThreads": [],
"allThreads": [],
"persistThread": [],
"toggleThreadSelector": []
}
// use this function to await responses
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
let result: Promise<WebviewMessage & { type: C }> = new Promise((res, rej) => {
awaiting[c].push(res)
})
return result
}
export const resolveAwaitingVSCodeResponse = (m: WebviewMessage) => {
// resolve all promises for this message
for (let res of awaiting[m.type]) {
res(m)
awaiting[m.type].splice(0) // clear the array
}
}
// VS Code exposes the function acquireVsCodeApi() to us, it should only get called once
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
type AcquireVsCodeApiType = () => {
postMessage(message: WebviewMessage): void;
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
// getState(): any;
};
export function getVSCodeAPI(): ReturnType<AcquireVsCodeApiType> {
if (vsCodeApi)
return vsCodeApi;
try {
// @ts-expect-error
// eslint-disable-next-line no-undef
vsCodeApi = acquireVsCodeApi();
return vsCodeApi!;
} catch (error) {
console.error('Failed to acquire VS Code API:', error);
throw new Error('This script must be run in a VS Code webview context');
}
}
-20
View File
@@ -1,20 +0,0 @@
import * as React from "react"
import * as ReactDOM from "react-dom/client"
import Sidebar from "./Sidebar"
import { ThreadsProvider } from "./threadsContext"
// mount the sidebar on the id="root" element
if (typeof document === "undefined") {
console.log("index.tsx error: document was undefined")
}
const rootElement = document.getElementById("root")!
console.log("Void root Element:", rootElement)
const extension = (
<ThreadsProvider>
<Sidebar />
</ThreadsProvider>
)
const root = ReactDOM.createRoot(rootElement)
root.render(extension)
@@ -1,80 +0,0 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import { getVSCodeAPI } from "../getVscodeApi"
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
}
const COPY_FEEDBACK_TIMEOUT = 1000
// code block with toolbar (Apply, Copy, etc) at top
const BlockCode = ({
text,
toolbar,
hideToolbar = false,
className,
}: {
text: string
toolbar?: ReactNode
hideToolbar?: boolean
className?: string
}) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
setTimeout(() => {
setCopyButtonState(CopyButtonState.Copy)
}, COPY_FEEDBACK_TIMEOUT)
}
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
const defaultToolbar = (
<>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={onCopy}
>
{copyButtonState}
</button>
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyChanges", code: text })
}}
>
Apply
</button>
</>
)
return (
<div className="relative group">
{!hideToolbar && (
<div className="absolute top-0 right-0 invisible group-hover:visible">
<div className="flex space-x-2 p-2">{toolbar || defaultToolbar}</div>
</div>
)}
<div
className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${!hideToolbar ? "rounded-tl-none" : ""} ${className}`}
>
<pre className="p-2">{text}</pre>
</div>
</div>
)
}
export default BlockCode
-39
View File
@@ -1,39 +0,0 @@
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
font-size: var(--vscode-font-size);
}
.btn {
@apply cursor-pointer transition-colors;
&.btn-primary {
@apply bg-vscode-button-bg text-vscode-button-fg;
&:not(:disabled) {
@apply hover:bg-vscode-button-hoverBg;
}
}
&.btn-sm {
@apply px-3 py-1 text-sm;
}
&.btn-secondary {
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
&:not(:disabled) {
@apply hover:bg-vscode-button-secondary-hoverBg;
}
}
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
}
.input {
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
}
@@ -1,100 +0,0 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { ChatMessage, ChatThreads } from "../shared_types"
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
type ThreadsContextValue = {
readonly allThreads: ChatThreads | null,
readonly currentThread: ChatThreads[string] | null;
addMessageToHistory: (message: ChatMessage) => void;
switchToThread: (threadId: string) => void;
startNewThread: () => void;
}
const ThreadsContext = createContext<ThreadsContextValue>(undefined as unknown as ThreadsContextValue)
const createNewThread = () => ({
id: new Date().getTime().toString(),
createdAt: new Date().toISOString(),
messages: [],
})
// const [stateRef, setState] = useInstantState(initVal)
// setState instantly changes the value of stateRef instead of having to wait until the next render
const useInstantState = <T,>(initVal: T) => {
const stateRef = useRef<T>(initVal)
const [_, setS] = useState<T>(initVal)
const setState = useCallback((newVal: T) => {
setS(newVal);
stateRef.current = newVal;
}, [])
return [stateRef as React.RefObject<T>, setState] as const // make s.current readonly - setState handles all changes
}
export function ThreadsProvider({ children }: { children: ReactNode }) {
const [allThreads, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadId, setCurrentThreadId] = useInstantState<string | null>(null)
// this loads allThreads in on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: "getAllThreads" })
awaitVSCodeResponse('allThreads')
.then(response => {
setAllThreads(response.threads)
})
}, [setAllThreads])
return (
<ThreadsContext.Provider
value={{
allThreads: allThreads.current,
currentThread: currentThreadId.current === null || allThreads.current === null ? null : allThreads.current[currentThreadId.current],
addMessageToHistory: (message: ChatMessage) => {
let currentThread: ChatThreads[string]
if (!(currentThreadId.current === null || allThreads.current === null)) {
currentThread = allThreads.current[currentThreadId.current]
}
else {
currentThread = createNewThread()
setCurrentThreadId(currentThread.id)
}
setAllThreads({
...allThreads.current,
[currentThread.id]: {
...currentThread,
messages: [...currentThread.messages, message],
}
})
getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
},
switchToThread: (threadId: string) => {
setCurrentThreadId(threadId);
},
startNewThread: () => {
const newThread = createNewThread()
setAllThreads({
...allThreads.current,
[newThread.id]: newThread
})
setCurrentThreadId(newThread.id)
},
}}
>
{children}
</ThreadsContext.Provider>
)
}
export function useThreads(): ThreadsContextValue {
const context = useContext<ThreadsContextValue>(ThreadsContext)
if (context === undefined) {
throw new Error("useThreads must be used within a ThreadsProvider")
}
return context
}
@@ -1,15 +0,0 @@
import * as assert from 'assert';
// You can import and use all API from the 'vscode' module
// as well as import your extension to test it
import * as vscode from 'vscode';
// import * as myExtension from '../../extension';
suite('Extension Test Suite', () => {
vscode.window.showInformationMessage('Start all tests.');
test('Sample test', () => {
assert.strictEqual(-1, [1, 2, 3].indexOf(5));
assert.strictEqual(-1, [1, 2, 3].indexOf(0));
});
});
-28
View File
@@ -1,28 +0,0 @@
/** @type {import('tailwindcss').Config} */
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
module.exports = {
content: ["./src/sidebar/**/*.{html,js,ts,jsx,tsx}"],
theme: {
extend: {
colors: {
vscode: {
"sidebar-bg": "var(--vscode-sideBar-background)",
"editor-bg": "var(--vscode-editor-background)",
"editor-fg": "var(--vscode-editor-foreground)",
"input-bg": "var(--vscode-input-background)",
"input-fg": "var(--vscode-input-foreground)",
"input-border": "var(--vscode-input-border)",
"button-fg": "var(--vscode-button-foreground)",
"button-bg": "var(--vscode-button-background)",
"button-hoverBg": "var(--vscode-button-hoverBackground)",
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
"button-secondary-hoverBg":
"var(--vscode-button-secondaryHoverBackground)",
},
},
},
},
plugins: [],
};
-30
View File
@@ -1,30 +0,0 @@
{
"include": [
"src/**/*"
],
"exclude": [
"node_modules"
],
"compilerOptions": {
"jsx": "react",
"module": "Node16",
"target": "ES6",
"outDir": "out",
"types": [
"node",
"mocha",
],
"lib": [
"dom",
"es6",
"dom.iterable",
],
"sourceMap": true,
"rootDir": "src",
"strict": true, /* enable all strict type-checking options */
/* Additional Checks */
// "noImplicitReturns": true, /* Report error when not all code paths in function return a value. */
// "noFallthroughCasesInSwitch": true, /* Report errors for fallthrough cases in switch statement. */
// "noUnusedParameters": true, /* Report errors on unused parameters. */
}
}
+6966 -3407
View File
File diff suppressed because it is too large Load Diff
+28 -1
View File
@@ -9,6 +9,8 @@
"main": "./out/main",
"private": true,
"scripts": {
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
"watchreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js --watch && cd ../../../../../../../",
"test": "echo Please run any of the test scripts from the scripts folder.",
"test-browser": "npx playwright install && node test/unit/browser/index.js",
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
@@ -72,9 +74,13 @@
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
},
"dependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@google/generative-ai": "^0.21.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@parcel/watcher": "2.1.0",
"@rrweb/record": "^2.0.0-alpha.17",
"@rrweb/types": "^2.0.0-alpha.17",
"@vscode/deviceid": "^0.1.1",
"@vscode/iconv-lite-umd": "0.7.0",
"@vscode/policy-watcher": "^1.1.4",
@@ -85,6 +91,7 @@
"@vscode/sudo-prompt": "9.3.1",
"@vscode/tree-sitter-wasm": "^0.0.4",
"@vscode/vscode-languagedetection": "1.0.21",
"@vscode/webview-ui-toolkit": "^1.4.0",
"@vscode/windows-mutex": "^0.5.0",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
@@ -96,16 +103,26 @@
"@xterm/addon-webgl": "^0.19.0-beta.64",
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"ajv": "^8.17.1",
"diff": "^7.0.0",
"groq-sdk": "^0.9.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
"jschardet": "3.1.3",
"kerberos": "2.1.1",
"lucide-react": "^0.460.0",
"minimist": "^1.2.6",
"native-is-elevated": "0.7.0",
"native-keymap": "^3.3.5",
"native-watchdog": "^1.4.1",
"node-pty": "1.1.0-beta21",
"ollama": "^0.5.11",
"open": "^8.4.2",
"openai": "^4.76.1",
"posthog-node": "^4.3.1",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.6.1",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-oniguruma": "1.7.0",
@@ -119,12 +136,17 @@
"@swc/core": "1.3.62",
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
"@types/diff": "^6.0.0",
"@types/eslint": "^9.6.1",
"@types/gulp-svgmin": "^1.2.1",
"@types/http-proxy-agent": "^2.0.1",
"@types/kerberos": "^1.1.2",
"@types/minimist": "^1.2.1",
"@types/mocha": "^9.1.1",
"@types/node": "20.x",
"@types/react": "^18.3.12",
"@types/react-dom": "^18.3.1",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/sinon": "^10.0.2",
"@types/sinon-test": "^2.4.2",
"@types/trusted-types": "^1.0.6",
@@ -190,6 +212,7 @@
"istanbul-lib-source-maps": "^4.0.1",
"istanbul-reports": "^3.1.5",
"lazy.js": "^0.4.2",
"marked": "^15.0.0",
"merge-options": "^1.0.1",
"mime": "^1.4.1",
"minimatch": "^3.0.4",
@@ -197,6 +220,7 @@
"mocha": "^10.2.0",
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"nodemon": "^3.1.9",
"npm-run-all": "^4.1.5",
"opn": "^6.0.0",
"original-fs": "^1.2.0",
@@ -207,16 +231,19 @@
"postcss-nesting": "^12.0.2",
"pump": "^1.0.1",
"rcedit": "^1.1.0",
"rimraf": "^2.2.8",
"rimraf": "^2.7.1",
"scope-tailwind": "^1.0.5",
"sinon": "^12.0.1",
"sinon-test": "^3.1.3",
"source-map": "0.6.1",
"source-map-support": "^0.3.2",
"style-loader": "^3.3.2",
"tailwindcss": "^3.4.14",
"ts-loader": "^9.4.2",
"ts-node": "^10.9.1",
"tsec": "0.2.7",
"tslib": "^2.6.3",
"tsup": "^8.3.5",
"typescript": "^5.7.0-dev.20240903",
"util": "^0.12.4",
"webpack": "^5.94.0",
+5 -50
View File
@@ -31,54 +31,9 @@
"nodejsRepository": "https://nodejs.org",
"urlProtocol": "code-oss",
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
"builtInExtensions": [
{
"name": "ms-vscode.js-debug-companion",
"version": "1.1.3",
"sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93",
"repo": "https://github.com/microsoft/vscode-js-debug-companion",
"metadata": {
"id": "99cb0b7f-7354-4278-b8da-6cc79972169d",
"publisherId": {
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
"publisherName": "ms-vscode",
"displayName": "Microsoft",
"flags": "verified"
},
"publisherDisplayName": "Microsoft"
}
},
{
"name": "ms-vscode.js-debug",
"version": "1.93.0",
"sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a",
"repo": "https://github.com/microsoft/vscode-js-debug",
"metadata": {
"id": "25629058-ddac-4e17-abba-74678e126c5d",
"publisherId": {
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
"publisherName": "ms-vscode",
"displayName": "Microsoft",
"flags": "verified"
},
"publisherDisplayName": "Microsoft"
}
},
{
"name": "ms-vscode.vscode-js-profile-table",
"version": "1.0.9",
"sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6",
"repo": "https://github.com/microsoft/vscode-js-profile-visualizer",
"metadata": {
"id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb",
"publisherId": {
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
"publisherName": "ms-vscode",
"displayName": "Microsoft",
"flags": "verified"
},
"publisherDisplayName": "Microsoft"
}
}
]
"extensionsGallery": {
"serviceUrl": "https://open-vsx.org/vscode/gallery",
"itemUrl": "https://open-vsx.org/vscode/item"
},
"builtInExtensions": []
}
+1 -1
View File
@@ -123,7 +123,7 @@ protocol.registerSchemesAsPrivileged([
},
{
scheme: 'vscode-file',
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true }
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true, }
}
]);
+6 -1
View File
@@ -1,6 +1,7 @@
{
"extends": "./tsconfig.base.json",
"compilerOptions": {
// "jsx": "react", // <-- Void added this
"esModuleInterop": true,
"removeComments": false,
"preserveConstEnums": true,
@@ -51,6 +52,10 @@
"./typings",
"./vs/**/*.ts",
"vscode-dts/vscode.proposed.*.d.ts",
"vscode-dts/vscode.d.ts"
"vscode-dts/vscode.d.ts",
// Void added these:
// "./vs/workbench/contrib/void/browser/**.tsx",
// "./vs/**/*.d.mts",
]
}
+24 -1
View File
@@ -121,6 +121,10 @@ import { normalizeNFC } from '../../base/common/normalization.js';
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
import { IMetricsService } from '../../platform/void/common/metricsService.js';
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
/**
* The main VS Code application. There will only ever be one instance,
* even if the user starts many instances (e.g. from the command line).
@@ -148,7 +152,7 @@ export class CodeApplication extends Disposable {
@IStateService private readonly stateService: IStateService,
@IFileService private readonly fileService: IFileService,
@IProductService private readonly productService: IProductService,
@IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService
@IUserDataProfilesMainService private readonly userDataProfilesMainService: IUserDataProfilesMainService,
) {
super();
@@ -508,6 +512,16 @@ export class CodeApplication extends Disposable {
});
//#endregion
// //#region Void IPC
// validatedIpcMain.handle('vscode:sendLLMMessage', async (event, data) => {
// try {
// await this.sendLLMMessage(data);
// } catch (error) {
// console.error('Error sending LLM message:', error);
// }
// });
// //#endregion
}
private onUnexpectedError(error: Error): void {
@@ -1091,6 +1105,9 @@ export class CodeApplication extends Disposable {
services.set(ITelemetryService, NullTelemetryService);
}
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
// Default Extensions Profile Init
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
@@ -1225,6 +1242,12 @@ export class CodeApplication extends Disposable {
mainProcessElectronServer.registerChannel('logger', loggerChannel);
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
// Void - use loggerChannel as reference
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
// Extension Host Debug Broadcasting
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
@@ -3,6 +3,7 @@
* Licensed under the MIT License. See License.txt in the project root for license information.
*--------------------------------------------------------------------------------------------*/
import { Event } from '../../../base/common/event.js';
import { ICodeEditor, IDiffEditor } from '../editorBrowser.js';
import { IDecorationRenderOptions } from '../../common/editorCommon.js';
@@ -0,0 +1,132 @@
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 { IModelDeltaDecoration } from '../../../common/model.js';
import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
import { IRange } from '../../../common/core/range.js';
import { EditorOption } from '../../../common/config/editorOptions.js';
// THIS FILE IS OLD!!!
export interface IInlineDiffService {
readonly _serviceBrand: undefined;
addDiff(editor: ICodeEditor, originalText: string, modifiedRange: IRange): void;
removeDiffs(editor: ICodeEditor): void;
}
export const IInlineDiffService = createDecorator<IInlineDiffService>('inlineDiffServiceOld');
class InlineDiffService extends Disposable implements IInlineDiffService {
private readonly _diffDecorations = new Map<ICodeEditor, string[]>();
private readonly _diffZones = new Map<ICodeEditor, string[]>();
_serviceBrand: undefined;
constructor() {
super();
}
initStream() {
}
public addDiff: IInlineDiffService['addDiff'] = (editor, originalText, modifiedRange) => {
// Clear existing diffs
this.removeDiffs(editor);
// green decoration and gutter decoration
const greenDecoration: IModelDeltaDecoration[] = [{
range: modifiedRange,
options: {
className: 'line-insert', // .monaco-editor .line-insert
description: 'line-insert',
isWholeLine: true,
minimap: {
color: { id: 'minimapGutter.addedBackground' },
position: 2
},
overviewRuler: {
color: { id: 'editorOverviewRuler.addedForeground' },
position: 7
}
}
}];
this._diffDecorations.set(editor, editor.deltaDecorations([], greenDecoration));
// red in a view zone
editor.changeViewZones(accessor => {
// Get the editor's font info
const fontInfo = editor.getOption(EditorOption.fontInfo);
const domNode = document.createElement('div');
// domNode.className = 'monaco-editor view-zones line-delete monaco-mouse-cursor-text';
domNode.style.fontSize = `${fontInfo.fontSize}px`;
domNode.style.fontFamily = fontInfo.fontFamily;
domNode.style.lineHeight = `${fontInfo.lineHeight}px`;
// div
const lineContent = document.createElement('div');
// lineContent.className = 'view-line'; // .monaco-editor .inline-deleted-text
// span
const contentSpan = document.createElement('span');
// span
const codeSpan = document.createElement('span');
// codeSpan.className = 'mtk1'; // char-delete
codeSpan.textContent = originalText;
// Mount
contentSpan.appendChild(codeSpan);
lineContent.appendChild(contentSpan);
domNode.appendChild(lineContent);
const viewZone: IViewZone = {
afterLineNumber: modifiedRange.startLineNumber - 1,
heightInLines: originalText.split('\n').length + 1,
domNode: domNode,
suppressMouseDown: true,
marginDomNode: this.createGutterElement()
};
const zoneId = accessor.addZone(viewZone);
// editor.layout();
this._diffZones.set(editor, [zoneId]);
});
}
// gutter is the thing to the left
private createGutterElement(): HTMLElement {
const gutterDiv = document.createElement('div');
gutterDiv.className = 'inline-diff-gutter';
const minusDiv = document.createElement('div');
minusDiv.className = 'inline-diff-deleted-gutter';
// minusDiv.textContent = '-';
gutterDiv.appendChild(minusDiv);
return gutterDiv;
}
public removeDiffs(editor: ICodeEditor): void {
const decorationIds = this._diffDecorations.get(editor) || [];
editor.deltaDecorations(decorationIds, []);
this._diffDecorations.delete(editor);
editor.changeViewZones(accessor => {
const zoneIds = this._diffZones.get(editor) || [];
zoneIds.forEach(id => accessor.removeZone(id));
});
this._diffZones.delete(editor);
}
override dispose(): void {
super.dispose();
this._diffDecorations.clear();
this._diffZones.clear();
}
}
registerSingleton(IInlineDiffService, InlineDiffService, InstantiationType.Eager);
@@ -425,3 +425,5 @@
margin: 0 4px;
}
}
@@ -22,7 +22,7 @@ export class ExpandLineSelectionAction extends EditorAction {
kbOpts: {
weight: KeybindingWeight.EditorCore,
kbExpr: EditorContextKeys.textInputFocus,
primary: KeyMod.CtrlCmd | KeyCode.KeyL
primary: KeyMod.CtrlCmd | KeyCode.KeyM // Void changed this to Cmd+M
},
});
}
@@ -0,0 +1,15 @@
// ---------- common ----------
// llmMessage
import '../common/llmMessageService.js'
// voidSettings
import '../common/voidSettingsService.js'
// refreshModel
import '../common/refreshModelService.js'
// metrics
import '../common/metricsService.js'
@@ -0,0 +1,174 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { generateUuid } from '../../../base/common/uuid.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { IVoidSettingsService } from './voidSettingsService.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
export interface ILLMMessageService {
readonly _serviceBrand: undefined;
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
abort: (requestId: string) => void;
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
}
export class LLMMessageService extends Disposable implements ILLMMessageService {
readonly _serviceBrand: undefined;
private readonly channel: IChannel // LLMMessageChannel
// llmMessage
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
// ollamaList
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
// openAICompatibleList
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
constructor(
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
// @INotificationService private readonly notificationService: INotificationService,
) {
super()
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
// llm
this._register((this.channel.listen('onText_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
this.onTextHooks_llm[e.requestId]?.(e)
}))
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
this.onFinalMessageHooks_llm[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
}))
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
console.log('Error in LLMMessageService:', JSON.stringify(e))
this.onErrorHooks_llm[e.requestId]?.(e)
this._onRequestIdDone(e.requestId)
}))
// ollama
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
this.onSuccess_ollama[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
this.onError_ollama[e.requestId]?.(e)
}))
// openaiCompatible
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
this.onSuccess_openAICompatible[e.requestId]?.(e)
}))
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
this.onError_openAICompatible[e.requestId]?.(e)
}))
}
sendLLMMessage(params: ServiceSendLLMMessageParams) {
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { featureName } = proxyParams
// end early if no provider
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (modelSelection === null) {
onError({ message: 'Please add a Provider in Settings!', fullError: null })
return null
}
const { providerName, modelName } = modelSelection
// add state for request id
const requestId_ = generateUuid();
this.onTextHooks_llm[requestId_] = onText
this.onFinalMessageHooks_llm[requestId_] = onFinalMessage
this.onErrorHooks_llm[requestId_] = onError
const { settingsOfProvider } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
requestId: requestId_,
providerName,
modelName,
settingsOfProvider,
} satisfies MainLLMMessageParams);
return requestId_
}
abort(requestId: string) {
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
this._onRequestIdDone(requestId)
}
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.onSuccess_ollama[requestId_] = onSuccess
this.onError_ollama[requestId_] = onError
this.channel.call('ollamaList', {
...proxyParams,
settingsOfProvider,
requestId: requestId_,
} satisfies MainModelListParams<OllamaModelResponse>)
}
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
const { onSuccess, onError, ...proxyParams } = params
const { settingsOfProvider } = this.voidSettingsService.state
// add state for request id
const requestId_ = generateUuid();
this.onSuccess_openAICompatible[requestId_] = onSuccess
this.onError_openAICompatible[requestId_] = onError
this.channel.call('openAICompatibleList', {
...proxyParams,
settingsOfProvider,
requestId: requestId_,
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
}
_onRequestIdDone(requestId: string) {
delete this.onTextHooks_llm[requestId]
delete this.onFinalMessageHooks_llm[requestId]
delete this.onErrorHooks_llm[requestId]
delete this.onSuccess_ollama[requestId]
delete this.onError_ollama[requestId]
}
}
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
@@ -0,0 +1,149 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { IRange } from '../../../editor/common/core/range'
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
export type OnError = (p: { message: string, fullError: Error | null }) => void
export type AbortRef = { current: (() => void) | null }
export type LLMMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
}
export type ServiceSendLLMFeatureParams = {
featureName: 'Ctrl+K';
range: IRange;
} | {
featureName: 'Ctrl+L';
} | {
featureName: 'Autocomplete';
range: IRange;
}
// params to the true sendLLMMessage function
export type LLMMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
abortRef: AbortRef;
messages: LLMMessage[];
logging: {
loggingName: string,
};
providerName: ProviderName;
modelName: string;
settingsOfProvider: SettingsOfProvider;
}
export type ServiceSendLLMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
messages: LLMMessage[];
logging: {
loggingName: string,
};
} & ServiceSendLLMFeatureParams
// can't send functions across a proxy, use listeners instead
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
export type MainLLMMessageParams = Omit<LLMMMessageParams, BlockedMainLLMMessageParams> & { requestId: string }
export type MainLLMMessageAbortParams = { requestId: string }
export type EventLLMMessageOnTextParams = Parameters<OnText>[0] & { requestId: string }
export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0] & { requestId: string }
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
export type _InternalSendLLMMessageFnType = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
settingsOfProvider: SettingsOfProvider;
providerName: ProviderName;
modelName: string;
_setAborter: (aborter: () => void) => void;
}) => void
// service -> main -> internal -> event (back to main)
// (browser)
// These are from 'ollama' SDK
interface OllamaModelDetails {
parent_model: string;
format: string;
family: string;
families: string[];
parameter_size: string;
quantization_level: string;
}
export type OllamaModelResponse = {
name: string;
modified_at: Date;
size: number;
digest: string;
details: OllamaModelDetails;
expires_at: Date;
size_vram: number;
}
export type OpenaiCompatibleModelResponse = {
id: string;
created: number;
object: 'model';
owned_by: string;
}
// params to the true list fn
export type ModelListParams<modelResponse> = {
settingsOfProvider: SettingsOfProvider;
onSuccess: (param: { models: modelResponse[] }) => void;
onError: (param: { error: string }) => void;
}
// params to the service
export type ServiceModelListParams<modelResponse> = {
onSuccess: (param: { models: modelResponse[] }) => void;
onError: (param: { error: any }) => void;
}
type BlockedMainModelListParams = 'onSuccess' | 'onError'
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
@@ -0,0 +1,39 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
export interface IMetricsService {
readonly _serviceBrand: undefined;
capture(event: string, params: Record<string, any>): void;
}
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
// implemented by calling channel
export class MetricsService implements IMetricsService {
readonly _serviceBrand: undefined;
private readonly metricsService: IMetricsService;
constructor(
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
) {
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
}
// call capture on the channel
capture(...params: Parameters<IMetricsService['capture']>) {
this.metricsService.capture(...params);
}
}
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
@@ -0,0 +1,192 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
type RefreshableState = ({
state: 'init',
timeoutId: null,
} | {
state: 'refreshing',
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
} | {
state: 'success',
timeoutId: null,
})
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
ollama: ['_enabled', 'endpoint'],
openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
}
const REFRESH_INTERVAL = 5_000
// const COOLDOWN_TIMEOUT = 300
// element-wise equals
function eq<T>(a: T[], b: T[]): boolean {
if (a.length !== b.length) return false
for (let i = 0; i < a.length; i++) {
if (a[i] !== b[i]) return false
}
return true
}
export interface IRefreshModelService {
readonly _serviceBrand: undefined;
refreshModels: (providerName: RefreshableProviderName) => Promise<void>;
onDidChangeState: Event<RefreshableProviderName>;
state: RefreshModelStateOfProvider;
}
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
export class RefreshModelService extends Disposable implements IRefreshModelService {
readonly _serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<RefreshableProviderName>();
readonly onDidChangeState: Event<RefreshableProviderName> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
constructor(
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
) {
super()
const disposables: Set<IDisposable> = new Set()
const initializePollingAndOnChange = () => {
this._clearAllTimeouts()
disposables.forEach(d => d.dispose())
disposables.clear()
if (!voidSettingsService.state.featureFlagSettings.autoRefreshModels) return
for (const providerName of refreshableProviderNames) {
const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
this.refreshModels(providerName, !enabled)
// every time providerName.enabled changes, refresh models too, like a useEffect
let relevantVals = () => refreshBasedOn[providerName].map(settingName => this.voidSettingsService.state.settingsOfProvider[providerName][settingName])
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
disposables.add(
this.voidSettingsService.onDidChangeState(() => { // we might want to debounce this
const newVals = relevantVals()
if (!eq(prevVals, newVals)) {
const prevEnabled = prevVals[0] as boolean
const enabled = newVals[0] as boolean
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
// if user just clicked enable, refresh
this.refreshModels(providerName, !enabled)
}
else {
// else if user just clicked disable, don't refresh
// //give cooldown before re-enabling (or at least re-fetching)
// const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT)
// this._setTimeoutId(providerName, timeoutId)
}
prevVals = newVals
}
})
)
}
}
// on mount (when get init settings state), and if a relevant feature flag changes (detected natively right now by refreshing if any flag changes), start refreshing models
voidSettingsService.waitForInitState.then(() => {
initializePollingAndOnChange()
this._register(
voidSettingsService.onDidChangeState((type) => { if (type === 'featureFlagSettings') initializePollingAndOnChange() })
)
})
}
state: RefreshModelStateOfProvider = {
ollama: { state: 'init', timeoutId: null },
openAICompatible: { state: 'init', timeoutId: null },
}
// start listening for models (and don't stop until success)
async refreshModels(providerName: RefreshableProviderName, enableProviderOnSuccess?: boolean) {
this._clearProviderTimeout(providerName)
// start loading models
this._setRefreshState(providerName, 'refreshing')
const fn = providerName === 'ollama' ? this.llmMessageService.ollamaList
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
: () => { }
fn({
onSuccess: ({ models }) => {
this.voidSettingsService.setDefaultModels(providerName, models.map(model => {
if (providerName === 'ollama') return (model as OllamaModelResponse).name
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id
else throw new Error('refreshMode fn: unknown provider', providerName)
}))
if (enableProviderOnSuccess) {
this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
}
this._setRefreshState(providerName, 'success')
},
onError: ({ error }) => {
// poll
console.log('retrying list models:', providerName, error)
const timeoutId = setTimeout(() => this.refreshModels(providerName, enableProviderOnSuccess), REFRESH_INTERVAL)
this._setTimeoutId(providerName, timeoutId)
}
})
}
_clearAllTimeouts() {
for (const providerName of refreshableProviderNames) {
this._clearProviderTimeout(providerName)
}
}
_clearProviderTimeout(providerName: RefreshableProviderName) {
// cancel any existing poll
if (this.state[providerName].timeoutId) {
clearTimeout(this.state[providerName].timeoutId)
this._setTimeoutId(providerName, null)
}
}
private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) {
this.state[providerName].timeoutId = timeoutId
}
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state']) {
this.state[providerName].state = state
this._onDidChangeState.fire(providerName)
}
}
registerSingleton(IRefreshModelService, RefreshModelService, InstantiationType.Eager);
@@ -0,0 +1,268 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../base/common/event.js';
import { Disposable } from '../../../base/common/lifecycle.js';
import { deepClone } from '../../../base/common/objects.js';
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
import { createDecorator } from '../../instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, FeatureFlagSettings, FeatureFlagName, defaultFeatureFlagSettings } from './voidSettingsTypes.js';
const STORAGE_KEY = 'void.voidSettingsStorage'
type SetSettingOfProviderFn = <S extends SettingName>(
providerName: ProviderName,
settingName: S,
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
) => Promise<void>;
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
featureName: K,
newVal: ModelSelectionOfFeature[K],
options?: { doNotApplyEffects?: true }
) => Promise<void>;
type SetFeatureFlagFn = (flagName: FeatureFlagName, newVal: boolean) => void;
export type ModelOption = { text: string, value: ModelSelection }
export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly featureFlagSettings: FeatureFlagSettings;
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
type EventProp = Exclude<keyof VoidSettingsState, '_modelOptions'> | 'all'
export interface IVoidSettingsService {
readonly _serviceBrand: undefined;
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
readonly waitForInitState: Promise<void>;
onDidChangeState: Event<EventProp>;
setSettingOfProvider: SetSettingOfProviderFn;
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
setFeatureFlag: SetFeatureFlagFn;
setDefaultModels(providerName: ProviderName, modelNames: string[]): void;
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
deleteModel(providerName: ProviderName, modelName: string): boolean;
}
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
let modelOptions: ModelOption[] = []
for (const providerName of providerNames) {
const providerConfig = settingsOfProvider[providerName]
if (!providerConfig._enabled) continue // if disabled, don't display model options
for (const { modelName, isHidden } of providerConfig.models) {
if (isHidden) continue
modelOptions.push({ text: `${modelName} (${providerName})`, value: { providerName, modelName } })
}
}
return modelOptions
}
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
featureFlagSettings: deepClone(defaultFeatureFlagSettings),
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
}
return d
}
export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidSettingsService');
class VoidSettingsService extends Disposable implements IVoidSettingsService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<EventProp>();
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
state: VoidSettingsState;
waitForInitState: Promise<void> // await this if you need a valid state initially
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IEncryptionService private readonly _encryptionService: IEncryptionService,
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
) {
super()
// at the start, we haven't read the partial config yet, but we need to set state to something
this.state = defaultState()
let resolver: () => void = () => { }
this.waitForInitState = new Promise((res, rej) => resolver = res)
// read and update the actual state immediately
this._readState().then(s => {
this.state = s
resolver()
this._onDidChangeState.fire('all')
})
}
private async _readState(): Promise<VoidSettingsState> {
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
if (!encryptedState)
return defaultState()
const stateStr = await this._encryptionService.decrypt(encryptedState)
return JSON.parse(stateStr)
}
private async _storeState() {
const state = this.state
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
}
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
const newSettingsOfProvider = {
...this.state.settingsOfProvider,
[providerName]: {
...this.state.settingsOfProvider[providerName],
[settingName]: newVal,
}
}
const newFeatureFlags = this.state.featureFlagSettings
// if changed models or enabled a provider, recompute models list
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
const newState: VoidSettingsState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
settingsOfProvider: newSettingsOfProvider,
featureFlagSettings: newFeatureFlags,
_modelOptions: newModelsList,
}
// this must go above this.setanythingelse()
this.state = newState
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
if (modelsListChanged) {
for (const featureName of featureNames) {
const currentSelection = newModelSelectionOfFeature[featureName]
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.value, currentSelection))
if (selnIdx === -1) {
if (newModelsList.length !== 0)
this.setModelSelectionOfFeature(featureName, newModelsList[0].value, { doNotApplyEffects: true })
else
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
}
}
}
await this._storeState()
this._onDidChangeState.fire('settingsOfProvider')
}
setFeatureFlag: SetFeatureFlagFn = async (flagName, newVal) => {
const newState = {
...this.state,
featureFlagSettings: {
...this.state.featureFlagSettings,
[flagName]: newVal
}
}
this.state = newState
await this._storeState()
this._onDidChangeState.fire('featureFlagSettings')
}
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
const newState: VoidSettingsState = {
...this.state,
modelSelectionOfFeature: {
...this.state.modelSelectionOfFeature,
[featureName]: newVal
}
}
this.state = newState
if (options?.doNotApplyEffects)
return
await this._storeState()
this._onDidChangeState.fire('modelSelectionOfFeature')
}
setDefaultModels(providerName: ProviderName, newDefaultModelNames: string[]) {
const { models } = this.state.settingsOfProvider[providerName]
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames)
const newModels = [
...newDefaultModels,
...models.filter(m => !m.isDefault), // keep any non-default models
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
toggleModelHidden(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
const modelIdx = models.findIndex(m => m.modelName === modelName)
if (modelIdx === -1) return
const newModels: VoidModelInfo[] = [
...models.slice(0, modelIdx),
{ ...models[modelIdx], isHidden: !models[modelIdx].isHidden },
...models.slice(modelIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
addModel(providerName: ProviderName, modelName: string) {
const { models } = this.state.settingsOfProvider[providerName]
const existingIdx = models.findIndex(m => m.modelName === modelName)
if (existingIdx !== -1) return // if exists, do nothing
const newModels = [
...models,
{ modelName, isDefault: false, isHidden: false }
]
this.setSettingOfProvider(providerName, 'models', newModels)
}
deleteModel(providerName: ProviderName, modelName: string): boolean {
const { models } = this.state.settingsOfProvider[providerName]
const delIdx = models.findIndex(m => m.modelName === modelName)
if (delIdx === -1) return false
const newModels = [
...models.slice(0, delIdx), // delete the idx
...models.slice(delIdx + 1, Infinity)
]
this.setSettingOfProvider(providerName, 'models', newModels)
return true
}
}
registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager);
@@ -0,0 +1,394 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
export type VoidModelInfo = {
modelName: string,
isDefault: boolean, // whether or not it's a default for its provider
isHidden: boolean, // whether or not the user is hiding it
}
export const modelInfoOfDefaultNames = (modelNames: string[]): VoidModelInfo[] => {
const isHidden = modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
return modelNames.map((modelName, i) => ({ modelName, isDefault: true, isHidden }))
}
// https://docs.anthropic.com/en/docs/about-claude/models
export const defaultAnthropicModels = modelInfoOfDefaultNames([
'claude-3-5-sonnet-20241022',
'claude-3-5-haiku-20241022',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
// 'claude-3-haiku-20240307',
])
// https://platform.openai.com/docs/models/gp
export const defaultOpenAIModels = modelInfoOfDefaultNames([
'o1-preview',
'o1-mini',
'gpt-4o',
'gpt-4o-mini',
// 'gpt-4o-2024-05-13',
// 'gpt-4o-2024-08-06',
// 'gpt-4o-mini-2024-07-18',
// 'gpt-4-turbo',
// 'gpt-4-turbo-2024-04-09',
// 'gpt-4-turbo-preview',
// 'gpt-4-0125-preview',
// 'gpt-4-1106-preview',
// 'gpt-4',
// 'gpt-4-0613',
// 'gpt-3.5-turbo-0125',
// 'gpt-3.5-turbo',
// 'gpt-3.5-turbo-1106',
])
// https://console.groq.com/docs/models
export const defaultGroqModels = modelInfoOfDefaultNames([
"mixtral-8x7b-32768",
"llama2-70b-4096",
"gemma-7b-it"
])
export const defaultGeminiModels = modelInfoOfDefaultNames([
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
])
// export const parseMaxTokensStr = (maxTokensStr: string) => {
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
// if (Number.isNaN(int))
// return undefined
// return int
// }
export const anthropicMaxPossibleTokens = (modelName: string) => {
if (modelName === 'claude-3-5-sonnet-20241022'
|| modelName === 'claude-3-5-haiku-20241022')
return 8192
if (modelName === 'claude-3-opus-20240229'
|| modelName === 'claude-3-sonnet-20240229'
|| modelName === 'claude-3-haiku-20240307')
return 4096
return 1024 // return a reasonably small number if they're using a different model
}
type UnionOfKeys<T> = T extends T ? keyof T : never;
export const defaultProviderSettings = {
anthropic: {
apiKey: '',
},
openAI: {
apiKey: '',
},
ollama: {
endpoint: 'http://127.0.0.1:11434',
},
openRouter: {
apiKey: '',
},
openAICompatible: {
endpoint: '',
apiKey: '',
},
gemini: {
apiKey: '',
},
groq: {
apiKey: ''
}
} as const
export type ProviderName = keyof typeof defaultProviderSettings
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
type CustomProviderSettings<providerName extends ProviderName> = {
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
}
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
}
type CommonProviderSettings = {
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
models: VoidModelInfo[],
}
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
// part of state
export type SettingsOfProvider = {
[providerName in ProviderName]: SettingsForProvider<providerName>
}
export type SettingName = keyof SettingsForProvider<ProviderName>
type DisplayInfoForProviderName = {
title: string,
}
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
if (providerName === 'anthropic') {
return {
title: 'Anthropic',
}
}
else if (providerName === 'openAI') {
return {
title: 'OpenAI',
}
}
else if (providerName === 'openRouter') {
return {
title: 'OpenRouter',
}
}
else if (providerName === 'ollama') {
return {
title: 'Ollama',
}
}
else if (providerName === 'openAICompatible') {
return {
title: 'OpenAI-Compatible',
}
}
else if (providerName === 'gemini') {
return {
title: 'Gemini',
}
}
else if (providerName === 'groq') {
return {
title: 'Groq',
}
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
type DisplayInfo = {
title: string,
placeholder: string,
subTextMd?: string,
}
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
if (settingName === 'apiKey') {
return {
title: 'API Key',
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
providerName === 'openAI' ? 'sk-proj-key...' :
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
providerName === 'gemini' ? 'key...' :
providerName === 'groq' ? 'gsk_key...' :
providerName === 'openAICompatible' ? 'sk-key...' :
'(never)',
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
providerName === 'openAICompatible' ? undefined :
undefined,
}
}
else if (settingName === 'endpoint') {
return {
title: providerName === 'ollama' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
: '(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: '(never)',
subTextMd: providerName === 'ollama' ? 'Read about Ollama [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
undefined,
}
}
else if (settingName === '_enabled') {
return {
title: '(never)',
placeholder: '(never)',
}
}
else if (settingName === 'models') {
return {
title: '(never)',
placeholder: '(never)',
}
}
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
}
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
}
export const voidInitModelOptions = {
anthropic: {
models: defaultAnthropicModels,
},
openAI: {
models: defaultOpenAIModels,
},
ollama: {
models: [],
},
openRouter: {
models: [], // any string
},
openAICompatible: {
models: [],
},
gemini: {
models: defaultGeminiModels,
},
groq: {
models: defaultGroqModels,
},
}
// used when waiting and for a type reference
export const defaultSettingsOfProvider: SettingsOfProvider = {
anthropic: {
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.anthropic,
...voidInitModelOptions.anthropic,
},
openAI: {
_enabled: undefined,
...defaultCustomSettings,
...defaultProviderSettings.openAI,
...voidInitModelOptions.openAI,
},
gemini: {
...defaultCustomSettings,
...defaultProviderSettings.gemini,
...voidInitModelOptions.gemini,
_enabled: undefined,
},
groq: {
...defaultCustomSettings,
...defaultProviderSettings.groq,
...voidInitModelOptions.groq,
_enabled: undefined,
},
ollama: {
...defaultCustomSettings,
...defaultProviderSettings.ollama,
...voidInitModelOptions.ollama,
_enabled: undefined,
},
openRouter: {
...defaultCustomSettings,
...defaultProviderSettings.openRouter,
...voidInitModelOptions.openRouter,
_enabled: undefined,
},
openAICompatible: {
...defaultCustomSettings,
...defaultProviderSettings.openAICompatible,
...voidInitModelOptions.openAICompatible,
_enabled: undefined,
},
}
export type ModelSelection = { providerName: ProviderName, modelName: string }
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
}
// this is a state
export type ModelSelectionOfFeature = {
'Ctrl+L': ModelSelection | null,
'Ctrl+K': ModelSelection | null,
'Autocomplete': ModelSelection | null,
}
export type FeatureName = keyof ModelSelectionOfFeature
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
// the models of these can be refreshed (in theory all can, but not all should)
export const refreshableProviderNames = ['ollama', 'openAICompatible'] satisfies ProviderName[]
export type RefreshableProviderName = typeof refreshableProviderNames[number]
export type FeatureFlagSettings = {
autoRefreshModels: boolean;
}
export const defaultFeatureFlagSettings: FeatureFlagSettings = {
autoRefreshModels: true,
}
export type FeatureFlagName = keyof FeatureFlagSettings
export const featureFlagNames = Object.keys(defaultFeatureFlagSettings) as FeatureFlagName[]
type FeatureFlagDisplayInfo = {
description: string,
}
export const displayInfoOfFeatureFlag = (featureFlag: FeatureFlagName): FeatureFlagDisplayInfo => {
if (featureFlag === 'autoRefreshModels') {
return {
description: `Automatically scan for and enable local models.`, // ${`refreshableProviderNames.map(providerName => titleOfProviderName(providerName)).join(', ')`}
}
}
throw new Error(`featureFlagInfo: Unknown feature flag: "${featureFlag}"`)
}
@@ -0,0 +1,71 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import Anthropic from '@anthropic-ai/sdk';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
// Anthropic
type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.anthropic
const maxTokens = anthropicMaxPossibleTokens(modelName)
if (maxTokens === undefined) {
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
return
}
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
// find system messages and concatenate them
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// remove system messages for Anthropic
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
const stream = anthropic.messages.stream({
system: systemMessage,
messages: anthropicMessages,
model: modelName,
max_tokens: maxTokens,
});
// when receive text
stream.on('text', (newText, fullText) => {
onText({ newText, fullText })
})
// when we get the final message on this stream (or when error/fail)
stream.on('finalMessage', (claude_response) => {
// stringify the response's content
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
onFinalMessage({ fullText: content })
})
stream.on('error', (error) => {
// the most common error will be invalid API key (401), so we handle this with a nice message
if (error instanceof Anthropic.APIError && error.status === 401) {
onError({ message: 'Invalid API key.', fullError: error })
}
else {
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
}
})
// TODO need to test this to make sure it works, it might throw an error
_setAborter(() => stream.controller.abort())
};
@@ -0,0 +1,53 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
// Gemini
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = ''
const thisConfig = settingsOfProvider.gemini
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
const model = genAI.getGenerativeModel({ model: modelName });
// remove system messages that get sent to Gemini
// str of all system messages
const systemMessage = messages
.filter(msg => msg.role === 'system')
.map(msg => msg.content)
.join('\n');
// Convert messages to Gemini format
const geminiMessages: Content[] = messages
.filter(msg => msg.role !== 'system')
.map((msg, i) => ({
parts: [{ text: msg.content }],
role: msg.role === 'assistant' ? 'model' : 'user'
}))
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
.then(async response => {
_setAborter(() => response.stream.return(fullText))
for await (const chunk of response.stream) {
const newText = chunk.text();
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
onError({ message: 'Invalid API key.', fullError: null });
}
else {
onError({ message: error + '', fullError: error });
}
})
}
@@ -0,0 +1,68 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
// // Greptile
// // https://docs.greptile.com/api-reference/query
// // https://docs.greptile.com/quickstart#sample-response-streamed
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
// let fullText = ''
// const thisConfig = settingsOfProvider.greptile
// fetch('https://api.greptile.com/v2/query', {
// method: 'POST',
// headers: {
// 'Authorization': `Bearer ${thisConfig.apikey}`,
// 'X-Github-Token': `${thisConfig.githubPAT}`,
// 'Content-Type': `application/json`,
// },
// body: JSON.stringify({
// messages,
// stream: true,
// repositories: [thisConfig.repoinfo],
// }),
// })
// // this is {message}\n{message}\n{message}...\n
// .then(async response => {
// const text = await response.text()
// console.log('got greptile', text)
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
// })
// // TODO make this actually stream, right now it just sends one message at the end
// // TODO add _setAborter() when add streaming
// .then(async responseArr => {
// for (const response of responseArr) {
// const type: string = response['type']
// const message = response['message']
// // when receive text
// if (type === 'message') {
// fullText += message
// onText({ newText: message, fullText })
// }
// else if (type === 'sources') {
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
// fullText += filepath
// onText({ newText: filepath, fullText })
// }
// // type: 'status' with an empty 'message' means last message
// else if (type === 'status') {
// if (!message) {
// onFinalMessage({ fullText })
// }
// }
// }
// })
// .catch(error => {
// onError({ error })
// });
// }
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import Groq from 'groq-sdk';
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
// Groq
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
let fullText = '';
const thisConfig = settingsOfProvider.groq
const groq = new Groq({
apiKey: thisConfig.apiKey,
dangerouslyAllowBrowser: true
});
await groq.chat.completions
.create({
messages: messages,
model: modelName,
stream: true,
temperature: 0.7,
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
})
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
if (newText) {
fullText += newText;
onText({ newText, fullText });
}
}
onFinalMessage({ fullText });
})
.catch(error => {
onError({ message: error + '', fullError: error });
})
};
@@ -0,0 +1,86 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Ollama } from 'ollama';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
onSuccess_({ models })
}
const onError = ({ error }: { error: string }) => {
onError_({ error })
}
try {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.list()
.then((response) => {
const { models } = response
onSuccess({ models })
})
.catch((error) => {
onError({ error: error + '' })
})
}
catch (error) {
onError({ error: error + '' })
}
}
// Ollama
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
const thisConfig = settingsOfProvider.ollama
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
let fullText = ''
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.chat({
model: modelName,
messages: messages,
stream: true,
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
})
.then(async stream => {
_setAborter(() => stream.abort())
// iterate through the stream
for await (const chunk of stream) {
const newText = chunk.message.content;
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
// when error/fail
.catch((error) => {
// if (typeof error === 'object') {
// const e = error.error as ErrorResponse['error']
// if (e) {
// const name = error.name ?? 'Error'
// onError({ error: `${name}: ${e}` })
// return;
// }
// }
onError({ message: error + '', fullError: error })
})
};
// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',]
@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import OpenAI from 'openai';
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
import { Model } from 'openai/resources/models.js';
// import { parseMaxTokensStr } from './util.js';
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
const onSuccess = ({ models }: { models: Model[] }) => {
onSuccess_({ models })
}
const onError = ({ error }: { error: string }) => {
onError_({ error })
}
try {
const thisConfig = settingsOfProvider.openAICompatible
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
openai.models.list()
.then(async (response) => {
const models: Model[] = []
models.push(...response.data)
while (response.hasNextPage()) {
models.push(...(await response.getNextPage()).data)
}
onSuccess({ models })
})
.catch((error) => {
onError({ error: error + '' })
})
}
catch (error) {
onError({ error: error + '' })
}
}
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
let fullText = ''
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
if (providerName === 'openAI') {
const thisConfig = settingsOfProvider.openAI
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openRouter') {
const thisConfig = settingsOfProvider.openRouter
openai = new OpenAI({
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
defaultHeaders: {
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
},
});
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider.openAICompatible
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
}
else {
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
throw new Error(`providerName was invalid: ${providerName}`)
}
openai.models.list()
openai.chat.completions
.create(options)
.then(async response => {
_setAborter(() => response.controller.abort())
// when receive text
for await (const chunk of response) {
const newText = chunk.choices[0]?.delta?.content || '';
fullText += newText;
onText({ newText, fullText });
}
onFinalMessage({ fullText });
})
// when error/fail - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError && error.status === 401) {
onError({ message: 'Invalid API key.', fullError: error });
}
else {
onError({ message: error, fullError: error });
}
})
};
@@ -0,0 +1,115 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicMsg } from './anthropic.js';
import { sendOllamaMsg } from './ollama.js';
import { sendOpenAIMsg } from './openai.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGroqMsg } from './groq.js';
export const sendLLMMessage = ({
messages,
onText: onText_,
onFinalMessage: onFinalMessage_,
onError: onError_,
abortRef: abortRef_,
logging: { loggingName },
settingsOfProvider,
providerName,
modelName,
}: LLMMMessageParams,
metricsService: IMetricsService
) => {
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = (eventId: string, extras?: object) => {
metricsService.capture(eventId, {
providerName,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
version: '2024-11-14',
...extras,
})
}
const submit_time = new Date()
let _fullTextSoFar = ''
let _aborter: (() => void) | null = null
let _setAborter = (fn: () => void) => { _aborter = fn }
let _didAbort = false
const onText: OnText = ({ newText, fullText }) => {
if (_didAbort) return
onText_({ newText, fullText })
_fullTextSoFar = fullText
}
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
if (_didAbort) return
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
onFinalMessage_({ fullText })
}
const onError: OnError = ({ message: error, fullError }) => {
if (_didAbort) return
console.log("ERROR!!!!!", error)
console.error('sendLLMMessage onError:', error)
captureChatEvent(`${loggingName} - Error`, { error })
onError_({ message: error, fullError })
}
const onAbort = () => {
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
try { _aborter?.() } // aborter sometimes automatically throws an error
catch (e) { }
_didAbort = true
}
abortRef_.current = onAbort
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
try {
switch (providerName) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
break;
default:
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
break;
}
}
catch (error) {
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
// ; (_aborter as any)?.()
// _didAbort = true
}
}
@@ -0,0 +1,140 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
// registered in app.ts
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../base/common/event.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
import { ollamaList } from './llmMessage/ollama.js';
import { openaiCompatibleList } from './llmMessage/openai.js';
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
export class LLMMessageChannel implements IServerChannel {
// sendLLMMessage
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
// abort
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
// ollamaList
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
// openaiCompatibleList
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
// stupidly, channels can't take in @IService
constructor(
private readonly metricsService: IMetricsService,
) { }
// browser uses this to listen for changes
listen(_: unknown, event: string): Event<any> {
if (event === 'onText_llm') {
return this._onText_llm.event;
}
else if (event === 'onFinalMessage_llm') {
return this._onFinalMessage_llm.event;
}
else if (event === 'onError_llm') {
return this._onError_llm.event;
}
else if (event === 'onSuccess_ollama') {
return this._onSuccess_ollama.event;
}
else if (event === 'onError_ollama') {
return this._onError_ollama.event;
}
else if (event === 'onSuccess_openAICompatible') {
return this._onSuccess_openAICompatible.event;
}
else if (event === 'onError_openAICompatible') {
return this._onError_openAICompatible.event;
}
else {
throw new Error(`Event not found: ${event}`);
}
}
// browser uses this to call
async call(_: unknown, command: string, params: any): Promise<any> {
try {
if (command === 'sendLLMMessage') {
this._callSendLLMMessage(params)
}
else if (command === 'abort') {
this._callAbort(params)
}
else if (command === 'ollamaList') {
this._callOllamaList(params)
}
else if (command === 'openAICompatibleList') {
this._callOpenAICompatibleList(params)
}
else {
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
}
}
catch (e) {
console.log('llmMessageChannel: Call Error:', e)
}
}
// the only place sendLLMMessage is actually called
private async _callSendLLMMessage(params: MainLLMMessageParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId_llm))
this._abortRefOfRequestId_llm[requestId] = { current: null }
const mainThreadParams: LLMMMessageParams = {
...params,
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
abortRef: this._abortRefOfRequestId_llm[requestId],
}
sendLLMMessage(mainThreadParams, this.metricsService);
}
private _callAbort(params: MainLLMMessageAbortParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId_llm)) return
this._abortRefOfRequestId_llm[requestId].current?.()
delete this._abortRefOfRequestId_llm[requestId]
}
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
const { requestId } = params;
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
...params,
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
}
ollamaList(mainThreadParams)
}
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
const { requestId } = params;
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
...params,
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
}
openaiCompatibleList(mainThreadParams)
}
}
@@ -0,0 +1,46 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../base/common/lifecycle.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node'
// posthog-js (old):
// posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
// const buildEnv = 'development';
// const buildNumber = '1.0.0';
// const isMac = process.platform === 'darwin';
export class MetricsMainService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
readonly _distinctId: string
readonly client: PostHog
constructor(
@ITelemetryService private readonly _telemetryService: ITelemetryService
) {
super()
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { host: 'https://us.i.posthog.com', })
const { devDeviceId, firstSessionDate, machineId } = this._telemetryService
this._distinctId = devDeviceId
this.client.identify({ distinctId: devDeviceId, properties: { firstSessionDate, machineId } })
console.log('Void posthog metrics info:', JSON.stringify({ devDeviceId, firstSessionDate, machineId }))
}
capture: IMetricsService['capture'] = (event, params) => {
const capture = { distinctId: this._distinctId, event, properties: params } as const
// console.log('full capture:', capture)
this.client.capture(capture)
}
}
@@ -741,8 +741,12 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
});
}
private marketplaceHeadersPromise: Promise<object> | undefined;
private getMarketplaceHeaders(): Promise<object> {
if (!this.marketplaceHeadersPromise) {
@@ -90,6 +90,9 @@ import './mainThreadProfileContentHandlers.js';
import './mainThreadAiRelatedInformation.js';
import './mainThreadAiEmbeddingVector.js';
// Void added this:
import './mainThreadInlineDiff.js';
export class ExtensionPoints implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.extensionPoints';
@@ -0,0 +1,131 @@
// Void created this file
// it comes from mainThreadCodeInsets.ts
import { Disposable } from '../../../base/common/lifecycle.js';
import { ICodeEditorService } from '../../../editor/browser/services/codeEditorService.js';
import { MainContext, MainThreadInlineDiffShape } from '../common/extHost.protocol.js';
import { IInlineDiffService } from '../../../editor/browser/services/inlineDiffService/inlineDiffService.js';
import { ICodeEditor } from '../../../editor/browser/editorBrowser.js';
import { IRange } from '../../../editor/common/core/range.js';
import { extHostNamedCustomer, IExtHostContext } from '../../services/extensions/common/extHostCustomers.js';
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType, UndoRedoGroup } from '../../../platform/undoRedo/common/undoRedo.js';
import { IBulkEditService } from '../../../editor/browser/services/bulkEditService.js';
import { WorkspaceEdit } from '../../../editor/common/languages.js';
// import { IHistoryService } from '../../services/history/common/history.js';
@extHostNamedCustomer(MainContext.MainThreadInlineDiff)
export class MainThreadInlineDiff extends Disposable implements MainThreadInlineDiffShape {
// private readonly _proxy: ExtHostEditorInsetsShape;
// private readonly _disposables = new DisposableStore();
constructor(
context: IExtHostContext,
@IInlineDiffService private readonly _inlineDiff: IInlineDiffService,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
// @IHistoryService private readonly _historyService: IHistoryService, // history service is the history of pressing alt left/right
@IUndoRedoService private readonly _undoRedoService: IUndoRedoService, // undoRedo service is the history of pressing ctrl+z
@IBulkEditService private readonly _bulkEditService: IBulkEditService,
) {
super();
// this._proxy = context.getProxy(ExtHostContext.ExtHostEditorInsets);
// this._wcHistoryService.addEntry()
}
_streamingState: { type: 'streaming'; editGroup: UndoRedoGroup } | { type: 'idle' } = { type: 'idle' }
startStreaming(editorId: string) {
const editor = this._getEditor(editorId)
if (!editor) return
const model = editor.getModel()
if (!model) return
// all changes made when streaming should be a part of the group so we can undo them all together
this._streamingState = {
type: 'streaming',
editGroup: new UndoRedoGroup()
}
// TODO probably need to convert this to a stack
const diffsSnapshotBefore = { placeholder: '' }
const diffsSnapshotAfter = { placeholder: '' }
const elt: IUndoRedoElement = {
type: UndoRedoElementType.Resource,
resource: model.uri,
label: 'Add Diffs',
code: 'undoredo.inlineDiff',
undo: () => {
// reapply diffareas and diffs here
console.log('reverting diffareas...', diffsSnapshotBefore.placeholder)
},
redo: () => {
// reapply diffareas and diffs here
// when done, need to record diffSnapshotAfter
console.log('re-applying diffareas...', diffsSnapshotAfter.placeholder)
}
}
this._undoRedoService.pushElement(elt, this._streamingState.editGroup)
// ---------- START ----------
editor.updateOptions({ readOnly: true })
// ---------- WHEN DONE ----------
editor.updateOptions({ readOnly: false })
}
streamChange(editorId: string, edit: WorkspaceEdit) {
const editor = this._getEditor(editorId)
if (!editor) return
if (this._streamingState.type !== 'streaming') {
console.error('Expected streamChange to be in state \'streaming\'.')
return
}
// count all changes towards the group
this._bulkEditService.apply(edit, { undoRedoGroupId: this._streamingState.editGroup.id, })
}
_getEditor = (editorId: string): ICodeEditor | undefined => {
let editor: ICodeEditor | undefined;
editorId = editorId.substr(0, editorId.indexOf(',')); //todo@jrieken HACK
for (const candidate of this._editorService.listCodeEditors()) {
if (candidate.getId() === editorId
// && candidate.hasModel() && isEqual(candidate.getModel().uri, URI.revive(uri))
) {
editor = candidate;
break;
}
}
return editor
}
$addDiff(editorId: string, originalText: string, range: IRange): void {
const editor = this._getEditor(editorId);
if (!editor) return
this._inlineDiff.addDiff(editor, originalText, range)
}
}
@@ -109,6 +109,7 @@ import { ProxyIdentifier } from '../../services/extensions/common/proxyIdentifie
import { ExcludeSettingOptions, TextSearchCompleteMessageType, TextSearchContextNew, TextSearchMatchNew } from '../../services/search/common/searchExtTypes.js';
import type * as vscode from 'vscode';
import { ExtHostCodeMapper } from './extHostCodeMapper.js';
import { ExtHostInlineDiff } from './extHostInlineDiff.js';
export interface IExtensionRegistries {
mine: ExtensionDescriptionRegistry;
@@ -221,6 +222,9 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
const extHostSpeech = rpcProtocol.set(ExtHostContext.ExtHostSpeech, new ExtHostSpeech(rpcProtocol));
const extHostEmbeddings = rpcProtocol.set(ExtHostContext.ExtHostEmbeddings, new ExtHostEmbeddings(rpcProtocol));
// Void added this:
const extHostInlineDiff = rpcProtocol.set(ExtHostContext.ExtHostInlineDiff, new ExtHostInlineDiff(rpcProtocol.getProxy(MainContext.MainThreadInlineDiff), extHostEditors));
// Check that no named customers are missing
const expected = Object.values<ProxyIdentifier<any>>(ExtHostContext);
rpcProtocol.assertRegistered(expected);
@@ -519,6 +523,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
// namespace: languages
const languages: typeof vscode.languages = {
createDiagnosticCollection(name?: string): vscode.DiagnosticCollection {
return extHostDiagnostics.createDiagnosticCollection(extension.identifier, name);
},
@@ -553,6 +558,11 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
},
// Void added addInlineDiff here:
addInlineDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: vscode.Range): void {
extHostInlineDiff.addDiff(editor, originalText, modifiedRange)
},
// Void added this (I think will need to add this back when add ctrl+K)
// registerVoidCtrlKProvider(selector: vscode.DocumentSelector, provider: vscode.CodeLensProvider): vscode.Disposable {
// return extHostLanguageFeatures.registerCodeLensProvider(extension, checkSelector(selector), provider);
@@ -823,7 +833,7 @@ export function createApiFactoryAndRegisterActors(accessor: ServicesAccessor): I
return extHostWebviewPanels.createWebviewPanel(extension, viewType, title, showOptions, options);
},
createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): vscode.WebviewEditorInset {
checkProposedApiEnabled(extension, 'editorInsets');
// checkProposedApiEnabled(extension, 'editorInsets'); // Void commented this out
return extHostEditorInsets.createWebviewEditorInset(editor, line, height, options, extension);
},
createTerminal(nameOrOptions?: vscode.TerminalOptions | vscode.ExtensionTerminalOptions | string, shellPath?: string, shellArgs?: readonly string[] | string): vscode.Terminal {
@@ -2984,7 +2984,10 @@ export const MainContext = {
MainThreadTesting: createProxyIdentifier<MainThreadTestingShape>('MainThreadTesting'),
MainThreadLocalization: createProxyIdentifier<MainThreadLocalizationShape>('MainThreadLocalizationShape'),
MainThreadAiRelatedInformation: createProxyIdentifier<MainThreadAiRelatedInformationShape>('MainThreadAiRelatedInformation'),
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector')
MainThreadAiEmbeddingVector: createProxyIdentifier<MainThreadAiEmbeddingVectorShape>('MainThreadAiEmbeddingVector'),
// Void added this:
MainThreadInlineDiff: createProxyIdentifier<MainThreadInlineDiffShape>('MainThreadInlineDiff'),
};
export const ExtHostContext = {
@@ -3055,5 +3058,17 @@ export const ExtHostContext = {
ExtHostTimeline: createProxyIdentifier<ExtHostTimelineShape>('ExtHostTimeline'),
ExtHostTesting: createProxyIdentifier<ExtHostTestingShape>('ExtHostTesting'),
ExtHostTelemetry: createProxyIdentifier<ExtHostTelemetryShape>('ExtHostTelemetry'),
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization')
ExtHostLocalization: createProxyIdentifier<ExtHostLocalizationShape>('ExtHostLocalization'),
// Void added this:
ExtHostInlineDiff: createProxyIdentifier<ExtHostInlineDiffShape>('ExtHostInlineDiff'), // Void added this
};
// Void added these:
export interface ExtHostInlineDiffShape {
$onDidDispose(handle: number): void;
}
export interface MainThreadInlineDiffShape {
$addDiff(editorId: string, originalText: string, range: IRange): void;
}
@@ -0,0 +1,63 @@
// This file was created by Void
// reference extHostCodeInsets.ts
import { Emitter } from '../../../base/common/event.js';
import { DisposableStore } from '../../../base/common/lifecycle.js';
import { ExtHostInlineDiffShape, MainThreadInlineDiffShape } from './extHost.protocol.js';
import * as vscode from 'vscode'
import { ExtHostTextEditor } from './extHostTextEditor.js';
import { ExtHostEditors } from './extHostTextEditors.js';
import { Range } from '../../../workbench/api/common/extHostTypeConverters.js'
export class ExtHostInlineDiff implements ExtHostInlineDiffShape {
private readonly _disposables = new DisposableStore();
private _insets = new Map<number, { editor: vscode.TextEditor; inset: vscode.WebviewEditorInset; onDidReceiveMessage: Emitter<any> }>();
constructor(
private readonly _proxy: MainThreadInlineDiffShape,
private readonly _editors: ExtHostEditors,
) { }
dispose(): void {
this._insets.forEach(value => value.inset.dispose());
this._disposables.dispose();
}
addDiff(editor: vscode.TextEditor, originalText: string, modifiedRange: vscode.Range) {
let apiEditor: ExtHostTextEditor | undefined;
for (const candidate of this._editors.getVisibleTextEditors(true)) {
if (candidate.value === editor) {
apiEditor = <ExtHostTextEditor>candidate;
break;
}
}
if (!apiEditor) {
throw new Error('not a visible editor');
}
// can't send over the editor, so just send over its id and reconstruct it. This is stupid but it's what VSCode's editorinset does - Andrew
const id = apiEditor.id;
// let uri = apiEditor.value.document.uri;
// convert to IRange
const range = Range.from(modifiedRange)
this._proxy.$addDiff(id, originalText, range)
}
// main thread calls this when disposes diff with this particular handle
$onDidDispose(handle: number): void {
const value = this._insets.get(handle);
if (value) {
value.inset.dispose();
}
}
}
@@ -239,7 +239,7 @@ MenuRegistry.appendMenuItems([{
group: '3_workbench_layout_move',
command: {
id: ToggleSidebarPositionAction.ID,
title: localize('move second sidebar left', "Move Secondary Side Bar Left")
title: localize('move second sidebar left', "Move Void Side Bar Left")
},
when: ContextKeyExpr.and(ContextKeyExpr.notEquals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
order: 1
@@ -250,7 +250,7 @@ MenuRegistry.appendMenuItems([{
group: '3_workbench_layout_move',
command: {
id: ToggleSidebarPositionAction.ID,
title: localize('move second sidebar right', "Move Secondary Side Bar Right")
title: localize('move second sidebar right', "Move Void Side Bar Right")
},
when: ContextKeyExpr.and(ContextKeyExpr.equals('config.workbench.sideBar.location', 'right'), ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
order: 1
@@ -949,7 +949,7 @@ registerAction2(class extends Action2 {
if (!hasAddedView) {
results.push({
type: 'separator',
label: localize('secondarySideBarContainer', "Secondary Side Bar / {0}", containerModel.title)
label: localize('secondarySideBarContainer', "Void Side Bar / {0}", containerModel.title)
});
hasAddedView = true;
}
@@ -1056,7 +1056,7 @@ class MoveFocusedViewAction extends Action2 {
if (!(isViewSolo && currentLocation === ViewContainerLocation.AuxiliaryBar)) {
items.push({
id: '_.auxiliarybar.newcontainer',
label: localize('moveFocusedView.newContainerInSidePanel', "New Secondary Side Bar Entry")
label: localize('moveFocusedView.newContainerInSidePanel', "New Void Side Bar Entry")
});
}
@@ -1104,7 +1104,7 @@ class MoveFocusedViewAction extends Action2 {
items.push({
type: 'separator',
label: localize('secondarySideBar', "Secondary Side Bar")
label: localize('secondarySideBar', "Void Side Bar")
});
const pinnedAuxPanels = paneCompositePartService.getPinnedPaneCompositeIds(ViewContainerLocation.AuxiliaryBar);
@@ -1386,7 +1386,7 @@ if (!isMacintosh || !isNative) {
ToggleVisibilityActions.push(...[
CreateToggleLayoutItem(ToggleActivityBarVisibilityActionId, ContextKeyExpr.notEquals('config.workbench.activityBar.location', 'hidden'), localize('activityBar', "Activity Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: activityBarLeftIcon, iconB: activityBarRightIcon }),
CreateToggleLayoutItem(ToggleSidebarVisibilityAction.ID, SideBarVisibleContext, localize('sideBar', "Primary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelLeftIcon, iconB: panelRightIcon }),
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Secondary Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
CreateToggleLayoutItem(ToggleAuxiliaryBarAction.ID, AuxiliaryBarVisibleContext, localize('secondarySideBar', "Void Side Bar"), { whenA: ContextKeyExpr.equals('config.workbench.sideBar.location', 'left'), iconA: panelRightIcon, iconB: panelLeftIcon }),
CreateToggleLayoutItem(TogglePanelAction.ID, PanelVisibleContext, localize('panel', "Panel"), panelIcon),
CreateToggleLayoutItem(ToggleStatusbarVisibilityAction.ID, ContextKeyExpr.equals('config.workbench.statusBar.visible', true), localize('statusBar', "Status Bar"), statusBarIcon),
]);
+1 -1
View File
@@ -2434,7 +2434,7 @@ export abstract class Layout extends Disposable implements IWorkbenchLayoutServi
comment: 'Information about the layout of the workbench during statup';
activityBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the activity bar is visible' };
sideBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the primary side bar is visible' };
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the secondary side bar is visible' };
auxiliaryBarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the Void side bar is visible' };
panelVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the panel is visible' };
statusbarVisible: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether or the not the status bar is visible' };
sideBarPosition: { classification: 'SystemMetaData'; purpose: 'FeatureInsight'; comment: 'Whether the primary side bar is on the left or right' };
@@ -26,7 +26,7 @@ const auxiliaryBarLeftOffIcon = registerIcon('auxiliarybar-left-off-layout-icon'
export class ToggleAuxiliaryBarAction extends Action2 {
static readonly ID = 'workbench.action.toggleAuxiliaryBar';
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Secondary Side Bar Visibility");
static readonly LABEL = localize2('toggleAuxiliaryBar', "Toggle Void Side Bar Visibility");
constructor() {
super({
@@ -34,7 +34,7 @@ export class ToggleAuxiliaryBarAction extends Action2 {
title: ToggleAuxiliaryBarAction.LABEL,
toggled: {
condition: AuxiliaryBarVisibleContext,
title: localize('secondary sidebar', "Secondary Side Bar"),
title: localize('secondary sidebar', "Void Side Bar"),
mnemonicTitle: localize({ key: 'secondary sidebar mnemonic', comment: ['&& denotes a mnemonic'] }, "Secondary Si&&de Bar"),
},
@@ -70,7 +70,7 @@ registerAction2(ToggleAuxiliaryBarAction);
registerAction2(class FocusAuxiliaryBarAction extends Action2 {
static readonly ID = 'workbench.action.focusAuxiliaryBar';
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Secondary Side Bar");
static readonly LABEL = localize2('focusAuxiliaryBar', "Focus into Void Side Bar");
constructor() {
super({
@@ -103,7 +103,7 @@ MenuRegistry.appendMenuItems([
group: '0_workbench_toggles',
command: {
id: ToggleAuxiliaryBarAction.ID,
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarLeftIcon },
icon: auxiliaryBarLeftOffIcon,
},
@@ -116,7 +116,7 @@ MenuRegistry.appendMenuItems([
group: '0_workbench_toggles',
command: {
id: ToggleAuxiliaryBarAction.ID,
title: localize('toggleSecondarySideBar', "Toggle Secondary Side Bar"),
title: localize('toggleSecondarySideBar', "Toggle Void Side Bar"),
toggled: { condition: AuxiliaryBarVisibleContext, icon: auxiliaryBarRightIcon },
icon: auxiliaryBarRightOffIcon,
},
@@ -129,7 +129,7 @@ MenuRegistry.appendMenuItems([
group: '3_workbench_layout_move',
command: {
id: ToggleAuxiliaryBarAction.ID,
title: localize2('hideAuxiliaryBar', 'Hide Secondary Side Bar'),
title: localize2('hideAuxiliaryBar', 'Hide Void Side Bar'),
},
when: ContextKeyExpr.and(AuxiliaryBarVisibleContext, ContextKeyExpr.equals('viewLocation', ViewContainerLocationToString(ViewContainerLocation.AuxiliaryBar))),
order: 2
@@ -195,11 +195,13 @@ export class AuxiliaryBarPart extends AbstractPaneCompositePart {
const positionActions: IAction[] = [];
createAndFillInContextMenuActions(activityBarPositionMenu, { primary: [], secondary: positionActions });
// appears when right click
actions.push(...[
new Separator(),
new SubmenuAction('workbench.action.panel.position', localize('activity bar position', "Activity Bar Position"), positionActions),
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Secondary Side Bar Left") : localize('move second side bar right', "Move Secondary Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Secondary Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
toAction({ id: ToggleSidebarPositionAction.ID, label: currentPositionRight ? localize('move second side bar left', "Move Void Side Bar Left") : localize('move second side bar right', "Move Void Side Bar Right"), run: () => this.commandService.executeCommand(ToggleSidebarPositionAction.ID) }),
toAction({ id: ToggleAuxiliaryBarAction.ID, label: localize('hide second side bar', "Hide Void Side Bar"), run: () => this.commandService.executeCommand(ToggleAuxiliaryBarAction.ID) })
]);
}
@@ -4,82 +4,113 @@
*--------------------------------------------------------------------------------------------*/
import { localize } from '../../../../nls.js';
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { isMacintosh, isWeb, OS } from '../../../../base/common/platform.js';
import { Disposable, DisposableStore, IDisposable } from '../../../../base/common/lifecycle.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { IWorkspaceContextService, WorkbenchState } from '../../../../platform/workspace/common/workspace.js';
import { IConfigurationService } from '../../../../platform/configuration/common/configuration.js';
import { append, clearNode, $, h } from '../../../../base/browser/dom.js';
import { KeybindingLabel } from '../../../../base/browser/ui/keybindingLabel/keybindingLabel.js';
import { CommandsRegistry } from '../../../../platform/commands/common/commands.js';
import { ContextKeyExpr, ContextKeyExpression, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { editorForeground, registerColor, transparent } from '../../../../platform/theme/common/colorRegistry.js';
import { IThemeService } from '../../../../platform/theme/common/themeService.js';
import { ColorScheme } from '../../../../platform/theme/common/theme.js';
import { isRecentFolder, IWorkspacesService } from '../../../../platform/workspaces/common/workspaces.js';
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { OpenFileFolderAction, OpenFolderAction } from '../../actions/workspaceActions.js';
import { isMacintosh, isNative, OS } from '../../../../base/common/platform.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../contrib/void/browser/quickEditActions.js';
import { defaultKeybindingLabelStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { IWindowOpenable } from '../../../../platform/window/common/window.js';
import { ILabelService, Verbosity } from '../../../../platform/label/common/label.js';
import { splitRecentLabel } from '../../../../base/common/labels.js';
import { IHostService } from '../../../services/host/browser/host.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
// import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
registerColor('editorWatermark.foreground', { dark: transparent(editorForeground, 0.6), light: transparent(editorForeground, 0.68), hcDark: editorForeground, hcLight: editorForeground }, localize('editorLineHighlight', 'Foreground color for the labels in the editor watermark.'));
interface WatermarkEntry {
readonly text: string;
readonly id: string;
readonly mac?: boolean;
readonly when?: ContextKeyExpression;
}
// interface WatermarkEntry {
// readonly text: string;
// readonly id: string;
// readonly mac?: boolean;
// readonly when?: ContextKeyExpression;
// }
const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
// const showCommands: WatermarkEntry = { text: localize('watermark.showCommands', "Show All Commands"), id: 'workbench.action.showCommands' };
// const quickAccess: WatermarkEntry = { text: localize('watermark.quickAccess', "Go to File"), id: 'workbench.action.quickOpen' };
// const openFileNonMacOnly: WatermarkEntry = { text: localize('watermark.openFile', "Open File"), id: 'workbench.action.files.openFile', mac: false };
// const openFolderNonMacOnly: WatermarkEntry = { text: localize('watermark.openFolder', "Open Folder"), id: 'workbench.action.files.openFolder', mac: false };
// const openFileOrFolderMacOnly: WatermarkEntry = { text: localize('watermark.openFileFolder', "Open File or Folder"), id: 'workbench.action.files.openFileFolder', mac: true };
// const openRecent: WatermarkEntry = { text: localize('watermark.openRecent', "Open Recent"), id: 'workbench.action.openRecent' };
// const newUntitledFileMacOnly: WatermarkEntry = { text: localize('watermark.newUntitledFile', "New Untitled Text File"), id: 'workbench.action.files.newUntitledFile', mac: true };
// const findInFiles: WatermarkEntry = { text: localize('watermark.findInFiles', "Find in Files"), id: 'workbench.action.findInFiles' };
// const toggleTerminal: WatermarkEntry = { text: localize({ key: 'watermark.toggleTerminal', comment: ['toggle is a verb here'] }, "Toggle Terminal"), id: 'workbench.action.terminal.toggleTerminal', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
// const startDebugging: WatermarkEntry = { text: localize('watermark.startDebugging', "Start Debugging"), id: 'workbench.action.debug.start', when: ContextKeyExpr.equals('terminalProcessSupported', true) };
// const toggleFullscreen: WatermarkEntry = { text: localize({ key: 'watermark.toggleFullscreen', comment: ['toggle is a verb here'] }, "Toggle Full Screen"), id: 'workbench.action.toggleFullScreen' };
// const showSettings: WatermarkEntry = { text: localize('watermark.showSettings', "Show Settings"), id: 'workbench.action.openSettings' };
const noFolderEntries = [
showCommands,
openFileNonMacOnly,
openFolderNonMacOnly,
openFileOrFolderMacOnly,
openRecent,
newUntitledFileMacOnly
];
// // shown when Void is emtpty
// const noFolderEntries = [
// // showCommands,
// openFileNonMacOnly,
// openFolderNonMacOnly,
// openFileOrFolderMacOnly,
// openRecent,
// // newUntitledFileMacOnly
// ];
const folderEntries = [
showCommands,
quickAccess,
findInFiles,
startDebugging,
toggleTerminal,
toggleFullscreen,
showSettings
];
// const folderEntries = [
// showCommands,
// // quickAccess,
// // findInFiles,
// // startDebugging,
// // toggleTerminal,
// // toggleFullscreen,
// // showSettings
// ];
export class EditorGroupWatermark extends Disposable {
private readonly shortcuts: HTMLElement;
private readonly transientDisposables = this._register(new DisposableStore());
private enabled: boolean = false;
// private enabled: boolean = false;
private workbenchState: WorkbenchState;
private keybindingLabels = new Set<KeybindingLabel>();
private currentDisposables = new Set<IDisposable>();
constructor(
container: HTMLElement,
@IKeybindingService private readonly keybindingService: IKeybindingService,
@IWorkspaceContextService private readonly contextService: IWorkspaceContextService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService
// @IContextKeyService private readonly contextKeyService: IContextKeyService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IThemeService private readonly themeService: IThemeService,
@IWorkspacesService private readonly workspacesService: IWorkspacesService,
@ICommandService private readonly commandService: ICommandService,
@IHostService private readonly hostService: IHostService,
@ILabelService private readonly labelService: ILabelService,
) {
super();
const elements = h('.editor-group-watermark', [
h('.letterpress'),
h('.letterpress@icon'),
h('.shortcuts@shortcuts'),
]);
append(container, elements.root);
this.shortcuts = elements.shortcuts;
this.shortcuts = elements.shortcuts; // shortcuts div is modified on render()
// void icon style
const updateTheme = () => {
const theme = this.themeService.getColorTheme().type
const isDark = theme === ColorScheme.DARK || theme === ColorScheme.HIGH_CONTRAST_DARK
elements.icon.style.maxWidth = '220px'
elements.icon.style.opacity = '50%'
elements.icon.style.filter = isDark ? '' : 'invert(1)' //brightness(.5)
}
updateTheme()
this._register(
this.themeService.onDidColorThemeChange(updateTheme)
)
this.registerListeners();
@@ -103,56 +134,162 @@ export class EditorGroupWatermark extends Disposable {
this.render();
}));
const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
const allKeys = new Set<string>();
allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
this._register(this.contextKeyService.onDidChangeContext(e => {
if (e.affectsSome(allKeys)) {
this.render();
}
}));
// const allEntriesWhenClauses = [...noFolderEntries, ...folderEntries].filter(entry => entry.when !== undefined).map(entry => entry.when!);
// const allKeys = new Set<string>();
// allEntriesWhenClauses.forEach(when => when.keys().forEach(key => allKeys.add(key)));
// this._register(this.contextKeyService.onDidChangeContext(e => {
// if (e.affectsSome(allKeys)) {
// this.render();
// }
// }));
}
private render(): void {
const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
// const enabled = this.configurationService.getValue<boolean>('workbench.tips.enabled');
if (enabled === this.enabled) {
return;
}
// if (enabled === this.enabled) {
// return;
// }
// this.enabled = enabled;
// if (!enabled) {
// return;
// }
// const hasFolder = this.workbenchState !== WorkbenchState.EMPTY;
// const selected = (hasFolder ? folderEntries : noFolderEntries)
// .filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
// .filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
// .filter(entry => !!CommandsRegistry.getCommand(entry.id))
// .filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
this.enabled = enabled;
this.clear();
if (!enabled) {
return;
}
const box = append(this.shortcuts, $('.watermark-box'));
const folder = this.workbenchState !== WorkbenchState.EMPTY;
const selected = (folder ? folderEntries : noFolderEntries)
.filter(entry => !('when' in entry) || this.contextKeyService.contextMatchesRules(entry.when))
.filter(entry => !('mac' in entry) || entry.mac === (isMacintosh && !isWeb))
.filter(entry => !!CommandsRegistry.getCommand(entry.id))
.filter(entry => !!this.keybindingService.lookupKeybinding(entry.id));
const boxBelow = append(this.shortcuts, $(''))
const update = async () => {
const update = () => {
clearNode(box);
this.keybindingLabels.forEach(label => label.dispose());
this.keybindingLabels.clear();
clearNode(boxBelow);
for (const entry of selected) {
const keys = this.keybindingService.lookupKeybinding(entry.id);
if (!keys) {
continue;
this.currentDisposables.forEach(label => label.dispose());
this.currentDisposables.clear();
// Void - if the workbench is empty, show open
if (this.contextService.getWorkbenchState() === WorkbenchState.EMPTY) {
// Open Folder
const button = h('button')
button.root.textContent = 'Open Folder'
button.root.onclick = () => {
this.commandService.executeCommand(isMacintosh && isNative ? OpenFileFolderAction.ID : OpenFolderAction.ID)
// if (this.contextKeyService.contextMatchesRules(ContextKeyExpr.and(WorkbenchStateContext.isEqualTo('workspace')))) {
// this.commandService.executeCommand(OpenFolderViaWorkspaceAction.ID);
// } else {
// this.commandService.executeCommand(isMacintosh ? 'workbench.action.files.openFileFolder' : 'workbench.action.files.openFolder');
// }
}
box.appendChild(button.root);
// Recents
const recentlyOpened = await this.workspacesService.getRecentlyOpened()
.catch(() => ({ files: [], workspaces: [] })).then(w => w.workspaces);
box.append(
...recentlyOpened.map(w => {
let fullPath: string;
let windowOpenable: IWindowOpenable;
if (isRecentFolder(w)) {
windowOpenable = { folderUri: w.folderUri };
fullPath = w.label || this.labelService.getWorkspaceLabel(w.folderUri, { verbose: Verbosity.LONG });
}
else {
return null
// fullPath = w.label || this.labelService.getWorkspaceLabel(w.workspace, { verbose: Verbosity.LONG });
// windowOpenable = { workspaceUri: w.workspace.configPath };
}
const { name, parentPath } = splitRecentLabel(fullPath);
const li = $('li');
const link = $('button.button-link');
link.innerText = name;
link.title = fullPath;
link.setAttribute('aria-label', localize('welcomePage.openFolderWithPath', "Open folder {0} with path {1}", name, parentPath));
link.addEventListener('click', e => {
this.hostService.openWindow([windowOpenable], {
forceNewWindow: e.ctrlKey || e.metaKey,
remoteAuthority: w.remoteAuthority || null // local window if remoteAuthority is not set or can not be deducted from the openable
});
e.preventDefault();
e.stopPropagation();
});
li.appendChild(link);
const span = $('span');
span.classList.add('path');
span.classList.add('detail');
span.innerText = parentPath;
span.title = fullPath;
li.appendChild(span);
return li
}).filter(v => !!v)
)
}
else {
// show them Void keybindings
const keys = this.keybindingService.lookupKeybinding(VOID_CTRL_L_ACTION_ID);
const dl = append(box, $('dl'));
const dt = append(dl, $('dt'));
dt.textContent = entry.text;
dt.textContent = 'Chat'
const dd = append(dl, $('dd'));
const label = new KeybindingLabel(dd, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
label.set(keys);
this.keybindingLabels.add(label);
if (keys)
label.set(keys);
this.currentDisposables.add(label);
const keys2 = this.keybindingService.lookupKeybinding(VOID_CTRL_K_ACTION_ID);
const dl2 = append(box, $('dl'));
const dt2 = append(dl2, $('dt'));
dt2.textContent = 'Quick Edit'
const dd2 = append(dl2, $('dd'));
const label2 = new KeybindingLabel(dd2, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
if (keys2)
label2.set(keys2);
this.currentDisposables.add(label2);
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(boxBelow, $('button'));
button3.textContent = 'Void Settings'
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
if (keys3)
label3.set(keys3);
button3.onclick = () => {
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
}
this.currentDisposables.add(label3);
}
};
update();
@@ -167,6 +304,6 @@ export class EditorGroupWatermark extends Disposable {
override dispose(): void {
super.dispose();
this.clear();
this.keybindingLabels.forEach(label => label.dispose());
this.currentDisposables.forEach(label => label.dispose());
}
}
@@ -9,13 +9,15 @@
height: 100%;
}
.monaco-workbench .part.editor > .content .editor-group-container.empty {
opacity: 0.5; /* dimmed to indicate inactive state */
.monaco-workbench .part.editor > .content .editor-group-container.empty {
opacity: 0.5;
/* dimmed to indicate inactive state */
}
.monaco-workbench .part.editor > .content .editor-group-container.empty.active,
.monaco-workbench .part.editor > .content .editor-group-container.empty.dragged-over {
opacity: 1; /* indicate active/dragged-over group through undimmed state */
opacity: 1;
/* indicate active/dragged-over group through undimmed state */
}
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty.active:focus {
@@ -24,12 +26,13 @@
}
.monaco-workbench .part.editor > .content.empty .editor-group-container.empty.active:focus {
outline: none; /* never show outline for empty group if it is the last */
outline: none;
/* never show outline for empty group if it is the last */
}
/* Watermark & shortcuts */
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark {
display: flex;
height: 100%;
max-width: 290px;
@@ -49,26 +52,27 @@
height: calc(100% - 70px);
}
/* light */
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-watermark > .letterpress {
width: 100%;
max-height: 100%;
aspect-ratio: 1/1;
background-image: url('./letterpress-light.svg');
background-image: url('./void_cube_noshadow.png');
background-size: contain;
background-position-x: center;
background-repeat: no-repeat;
}
.monaco-workbench.vs-dark .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./letterpress-dark.svg');
background-image: url('./void_cube_noshadow.png');
}
.monaco-workbench.hc-light .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./letterpress-hcLight.svg');
background-image: url('./void_cube_noshadow.png');
}
.monaco-workbench.hc-black .part.editor > .content .editor-group-container .editor-group-watermark > .letterpress {
background-image: url('./letterpress-hcDark.svg');
background-image: url('./void_cube_noshadow.png');
}
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container > .editor-group-watermark > .shortcuts,
@@ -109,12 +113,13 @@
.monaco-workbench .part.editor > .content .editor-group-container > .title {
position: relative;
box-sizing: border-box;
box-sizing: border-box;
overflow: hidden;
}
.monaco-workbench .part.editor > .content .editor-group-container > .title:not(.tabs) {
display: flex; /* when tabs are not shown, use flex layout */
display: flex;
/* when tabs are not shown, use flex layout */
flex-wrap: nowrap;
}
@@ -144,7 +149,8 @@
.monaco-workbench .part.editor > .content .editor-group-container.empty.locked > .editor-group-container-toolbar,
.monaco-workbench .part.editor > .content:not(.empty) .editor-group-container.empty > .editor-group-container-toolbar,
.monaco-workbench .part.editor > .content.auxiliary .editor-group-container.empty > .editor-group-container-toolbar {
display: block; /* show toolbar when more than one editor group or always when auxiliary or locked */
display: block;
/* show toolbar when more than one editor group or always when auxiliary or locked */
}
.monaco-workbench .part.editor > .content .editor-group-container > .editor-group-container-toolbar .actions-container {
@@ -157,7 +163,7 @@
/* Editor */
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
.monaco-workbench .part.editor > .content .editor-group-container.empty > .editor-container {
display: none;
}
Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

@@ -342,7 +342,7 @@ registerAction2(class extends Action2 {
constructor() {
super({
id: 'workbench.action.closeAuxiliaryBar',
title: localize2('closeSecondarySideBar', 'Hide Secondary Side Bar'),
title: localize2('closeSecondarySideBar', 'Hide Void Side Bar'),
category: Categories.View,
icon: closeIcon,
menu: [{
@@ -415,14 +415,16 @@ class MoveViewsBetweenPanelsAction extends Action2 {
}
}
// --- Move Panel Views To Secondary Side Bar
// --- Move Panel Views To Void Side Bar
// these are just for the command pallette
class MovePanelToSidePanelAction extends MoveViewsBetweenPanelsAction {
static readonly ID = 'workbench.action.movePanelToSidePanel';
constructor() {
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
id: MovePanelToSidePanelAction.ID,
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
category: Categories.View,
f1: false
});
@@ -434,7 +436,7 @@ export class MovePanelToSecondarySideBarAction extends MoveViewsBetweenPanelsAct
constructor() {
super(ViewContainerLocation.Panel, ViewContainerLocation.AuxiliaryBar, {
id: MovePanelToSecondarySideBarAction.ID,
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Secondary Side Bar"),
title: localize2('movePanelToSecondarySideBar', "Move Panel Views To Void Side Bar"),
category: Categories.View,
f1: true
});
@@ -452,7 +454,7 @@ class MoveSidePanelToPanelAction extends MoveViewsBetweenPanelsAction {
constructor() {
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
id: MoveSidePanelToPanelAction.ID,
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
title: localize2('moveSidePanelToPanel', "Move Side Bar Views To Panel"), // Void - this seemed to have a typo before
category: Categories.View,
f1: false
});
@@ -465,7 +467,7 @@ export class MoveSecondarySideBarToPanelAction extends MoveViewsBetweenPanelsAct
constructor() {
super(ViewContainerLocation.AuxiliaryBar, ViewContainerLocation.Panel, {
id: MoveSecondarySideBarToPanelAction.ID,
title: localize2('moveSidePanelToPanel', "Move Secondary Side Bar Views To Panel"),
title: localize2('moveSidePanelToPanel', "Move Void Side Bar Views To Panel"),
category: Categories.View,
f1: true
});
@@ -512,7 +512,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
'type': 'string',
'enum': ['left', 'right'],
'default': 'left',
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The secondary side bar will show on the opposite side of the workbench.")
'description': localize('sideBarLocation', "Controls the location of the primary side bar and activity bar. They can either show on the left or right of the workbench. The Void side bar will show on the opposite side of the workbench.")
},
'workbench.panel.showLabel': {
'type': 'boolean',
@@ -545,12 +545,12 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
'type': 'string',
'enum': ['default', 'top', 'bottom', 'hidden'],
'default': 'default',
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Secondary Side Bars."),
'markdownDescription': localize({ comment: ['This is the description for a setting'], key: 'activityBarLocation' }, "Controls the location of the Activity Bar relative to the Primary and Void Side Bars."),
'enumDescriptions': [
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Secondary Side Bar."),
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Secondary Side Bars."),
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Secondary Side Bars."),
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Secondary Side Bars.")
localize('workbench.activityBar.location.default', "Show the Activity Bar on the side of the Primary Side Bar and on top of the Void Side Bar."),
localize('workbench.activityBar.location.top', "Show the Activity Bar on top of the Primary and Void Side Bars."),
localize('workbench.activityBar.location.bottom', "Show the Activity Bar at the bottom of the Primary and Void Side Bars."),
localize('workbench.activityBar.location.hide', "Hide the Activity Bar in the Primary and Void Side Bars.")
],
},
'workbench.activityBar.iconClickBehavior': {
@@ -598,7 +598,7 @@ const registry = Registry.as<IConfigurationRegistry>(ConfigurationExtensions.Con
'description': localize('workbench.hover.delay', "Controls the delay in milliseconds after which the hover is shown for workbench items (ex. some extension provided tree view items). Already visible items may require a refresh before reflecting this setting change."),
// Testing has indicated that on Windows and Linux 500 ms matches the native hovers most closely.
// On Mac, the delay is 1500.
'default': isMacintosh ? 1500 : 500,
'default': isMacintosh ? 300 : 300, // <-- Void edited this to 300 : 300 (was 1500 : 500)
'minimum': 0
},
'workbench.reduceMotion': {
@@ -151,7 +151,7 @@ export class ViewQuickAccessProvider extends PickerQuickAccessProvider<IViewQuic
// Viewlets / Panels
addPaneComposites(ViewContainerLocation.Sidebar, localize('views', "Side Bar"));
addPaneComposites(ViewContainerLocation.Panel, localize('panels', "Panel"));
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Secondary Side Bar"));
addPaneComposites(ViewContainerLocation.AuxiliaryBar, localize('secondary side bar', "Void Side Bar"));
const addPaneCompositeViews = (location: ViewContainerLocation) => {
const paneComposites = this.paneCompositeService.getPaneComposites(location);
@@ -0,0 +1,770 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { Position } from '../../../../editor/common/core/position.js';
import { InlineCompletion, InlineCompletionContext } from '../../../../editor/common/languages.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { Range } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from '../../../../platform/void/common/llmMessageService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { EditorResourceAccessor } from '../../../common/editor.js';
import { IModelService } from '../../../../editor/common/services/model.js';
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
/*
A summary of autotab:
Postprocessing
-one common problem for all models is outputting unbalanced parentheses
we solve this by trimming all extra closing parentheses from the generated string
in future, should make sure parentheses are always balanced
-another problem is completing the middle of a string, eg. "const [x, CURSOR] = useState()"
we complete up to first matchup character
but should instead complete the whole line / block (difficult because of parenthesis accuracy)
-too much info is bad. usually we want to show the user 1 line, and have a preloaded response afterwards
this should happen automatically with caching system
should break preloaded responses into \n\n chunks
Preprocessing
- we don't generate if cursor is at end / beginning of a line (no spaces)
- we generate 1 line if there is text to the right of cursor
- we generate 1 line if variable declaration
- (in many cases want to show 1 line but generate multiple)
State
- cache based on prefix (and do some trimming first)
- when press tab on one line, should have an immediate followup response
to do this, show autocompletes before they're fully finished
- [todo] remove each autotab when accepted
!- [todo] provide type information
Details
-generated results are trimmed up to 1 leading/trailing space
-prefixes are cached up to 1 trailing newline
-
*/
class LRUCache<K, V> {
public items: Map<K, V>;
private keyOrder: K[];
private maxSize: number;
private disposeCallback?: (value: V, key?: K) => void;
constructor(maxSize: number, disposeCallback?: (value: V, key?: K) => void) {
if (maxSize <= 0) throw new Error('Cache size must be greater than 0');
this.items = new Map();
this.keyOrder = [];
this.maxSize = maxSize;
this.disposeCallback = disposeCallback;
}
set(key: K, value: V): void {
// If key exists, remove it from the order list
if (this.items.has(key)) {
this.keyOrder = this.keyOrder.filter(k => k !== key);
}
// If cache is full, remove least recently used item
else if (this.items.size >= this.maxSize) {
const key = this.keyOrder[0];
const value = this.items.get(key);
// Call dispose callback if it exists
if (this.disposeCallback && value !== undefined) {
this.disposeCallback(value, key);
}
this.items.delete(key);
this.keyOrder.shift();
}
// Add new item
this.items.set(key, value);
this.keyOrder.push(key);
}
delete(key: K): boolean {
const value = this.items.get(key);
if (value !== undefined) {
// Call dispose callback if it exists
if (this.disposeCallback) {
this.disposeCallback(value, key);
}
this.items.delete(key);
this.keyOrder = this.keyOrder.filter(k => k !== key);
return true;
}
return false;
}
clear(): void {
// Call dispose callback for all items if it exists
if (this.disposeCallback) {
for (const [key, value] of this.items.entries()) {
this.disposeCallback(value, key);
}
}
this.items.clear();
this.keyOrder = [];
}
get size(): number {
return this.items.size;
}
has(key: K): boolean {
return this.items.has(key);
}
}
type AutocompletionStatus = 'pending' | 'finished' | 'error';
type Autocompletion = {
id: number,
prefix: string,
suffix: string,
startTime: number,
endTime: number | undefined,
status: AutocompletionStatus,
llmPromise: Promise<string> | undefined,
insertText: string,
requestId: string | null,
}
const DEBOUNCE_TIME = 500
const TIMEOUT_TIME = 60000
const MAX_CACHE_SIZE = 20
const MAX_PENDING_REQUESTS = 2
// postprocesses the result
const postprocessResult = (result: string) => {
// trim all whitespace except for a single leading/trailing space
// return result.trim()
const hasLeadingSpace = result.startsWith(' ');
const hasTrailingSpace = result.endsWith(' ');
return (hasLeadingSpace ? ' ' : '')
+ result.trim()
+ (hasTrailingSpace ? ' ' : '');
}
const extractCodeFromResult = (result: string) => {
// Match either:
// 1. ```language\n<code>```
// 2. ```<code>```
const match = result.match(/```(?:\w+\n)?([\s\S]*?)```|```([\s\S]*?)```/);
if (!match) {
return result;
}
// Return whichever group matched (non-empty)
return match[1] ?? match[2] ?? result;
}
// trims the end of the prefix to improve cache hit rate
const removeLeftTabsAndTrimEnd = (s: string): string => {
const trimmedString = s.trimEnd();
const trailingEnd = s.slice(trimmedString.length);
// keep only a single trailing newline
if (trailingEnd.includes('\n')) {
s = trimmedString + '\n';
}
s = s.replace(/^\s+/gm, ''); // remove left tabs
return s;
}
function getStringUpToUnbalancedParenthesis(s: string, prefix: string): string {
const pairs: Record<string, string> = { ')': '(', '}': '{', ']': '[' };
// process all bracets in prefix
let stack: string[] = []
const firstOpenIdx = prefix.search(/[[({]/);
if (firstOpenIdx !== -1) {
const brackets = prefix.slice(firstOpenIdx).split('').filter(c => '()[]{}'.includes(c));
for (const bracket of brackets) {
if (bracket === '(' || bracket === '{' || bracket === '[') {
stack.push(bracket);
} else {
if (stack.length > 0 && stack[stack.length - 1] === pairs[bracket]) {
stack.pop();
} else {
stack.push(bracket);
}
}
}
}
// iterate through each character
for (let i = 0; i < s.length; i++) {
const char = s[i];
if (char === '(' || char === '{' || char === '[') { stack.push(char); }
else if (char === ')' || char === '}' || char === ']') {
if (stack.length === 0 || stack.pop() !== pairs[char]) { return s.substring(0, i); }
}
}
return s;
}
const parenthesisChars = `{}()[]<>\`'"`
// returns the text in the autocompletion to display, assuming the prefix is already matched
const toInlineCompletions = ({ matchInfo, prefix, suffix, autocompletion, position, debug }: { matchInfo: matchInfo, prefix: string, suffix: string, autocompletion: Autocompletion, position: Position, debug?: boolean }): { insertText: string, range: Range }[] => {
const suffixLines = suffix.split('\n')
const prefixLines = prefix.split('\n')
const suffixToTheRightOfCursor = suffixLines[0]
const prefixToTheLeftOfCursor = prefixLines[prefixLines.length - 1]
const generatedMiddle = autocompletion.insertText
let startIdx = matchInfo.startIdx
let endIdx = generatedMiddle.length // exclusive bounds
// const naiveReturnValue = generatedMiddle.slice(startIdx)
// console.log('naiveReturnValue: ', JSON.stringify(naiveReturnValue))
// return [{ insertText: naiveReturnValue, }]
// do postprocessing for better ux
// this is a bit hacky but may change a lot
// if there is space at the start of the completion and user has added it, remove it
const charToLeftOfCursor = prefixToTheLeftOfCursor.slice(-1)[0] || ''
const userHasAddedASpace = charToLeftOfCursor === ' ' || charToLeftOfCursor === '\t'
const rawFirstNonspaceIdx = generatedMiddle.slice(startIdx).search(/[^\t ]/)
if (rawFirstNonspaceIdx > -1 && userHasAddedASpace) {
const firstNonspaceIdx = rawFirstNonspaceIdx + startIdx;
// console.log('p0', startIdx, rawFirstNonspaceIdx)
startIdx = Math.max(startIdx, firstNonspaceIdx)
}
// if user is on a blank line and the generation starts with newline(s), remove them
const numStartingNewlines = generatedMiddle.slice(startIdx).match(/^\n+/)?.[0].length || 0;
if (
!prefixToTheLeftOfCursor.trim()
&& !suffixToTheRightOfCursor.trim()
&& numStartingNewlines > 0
) {
// console.log('p1', numStartingNewlines)
startIdx += numStartingNewlines
}
// if the generated text matches with the suffix on the current line, stop
if (suffixToTheRightOfCursor.trim()) { // completing in the middle of a line
// complete until there is a match
const rawMatchIndex = generatedMiddle.slice(startIdx).lastIndexOf(suffixToTheRightOfCursor.trim()[0])
if (rawMatchIndex > -1) {
// console.log('p2', rawMatchIndex, startIdx, suffixToTheRightOfCursor.trim()[0], 'AAA', generatedMiddle.slice(startIdx))
const matchIdx = rawMatchIndex + startIdx;
const matchChar = generatedMiddle[matchIdx]
if (parenthesisChars.includes(matchChar)) {
endIdx = Math.min(endIdx, matchIdx)
}
}
}
const restOfLineToGenerate = generatedMiddle.slice(startIdx).split('\n')[0] ?? ''
// condition to complete as a single line completion
if (
prefixToTheLeftOfCursor.trim()
&& !suffixToTheRightOfCursor.trim()
&& restOfLineToGenerate.trim()
) {
const rawNewlineIdx = generatedMiddle.slice(startIdx).indexOf('\n')
if (rawNewlineIdx > -1) {
// console.log('p3', startIdx, rawNewlineIdx)
const newlineIdx = rawNewlineIdx + startIdx;
endIdx = Math.min(endIdx, newlineIdx)
}
}
// // if a generated line matches with a suffix line, stop
// if (suffixLines.length > 1) {
// console.log('4')
// const lines = []
// for (const generatedLine of generatedLines) {
// if (suffixLines.slice(0, 10).some(suffixLine =>
// generatedLine.trim() !== '' && suffixLine.trim() !== ''
// && generatedLine.trim().startsWith(suffixLine.trim())
// )) break;
// lines.push(generatedLine)
// }
// endIdx = lines.join('\n').length // this is hacky, remove or refactor in future
// }
// console.log('pFinal', startIdx, endIdx)
let completionStr = generatedMiddle.slice(startIdx, endIdx)
// filter out unbalanced parentheses
completionStr = getStringUpToUnbalancedParenthesis(completionStr, prefix)
// console.log('originalCompletionStr: ', JSON.stringify(generatedMiddle.slice(startIdx)))
// console.log('finalCompletionStr: ', JSON.stringify(completionStr))
let rangeToReplace: Range = new Range(position.lineNumber, position.column, position.lineNumber, position.column)
return [{
insertText: completionStr,
range: rangeToReplace,
}]
}
// returns whether this autocompletion is in the cache
// const doesPrefixMatchAutocompletion = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): boolean => {
// const originalPrefix = autocompletion.prefix
// const generatedMiddle = autocompletion.result
// const originalPrefixTrimmed = trimPrefix(originalPrefix)
// const currentPrefixTrimmed = trimPrefix(prefix)
// if (currentPrefixTrimmed.length < originalPrefixTrimmed.length) {
// return false
// }
// const isMatch = (originalPrefixTrimmed + generatedMiddle).startsWith(currentPrefixTrimmed)
// return isMatch
// }
const getPrefixAndSuffix = (model: ITextModel, position: Position) => {
const fullText = model.getValue();
const cursorOffset = model.getOffsetAt(position)
const prefix = fullText.substring(0, cursorOffset)
const suffix = fullText.substring(cursorOffset)
return { prefix, suffix }
}
const getIndex = (str: string, line: number, char: number) => {
return str.split('\n').slice(0, line).join('\n').length + (line > 0 ? 1 : 0) + char;
}
const getLastLine = (s: string): string => {
const matches = s.match(/[^\n]*$/)
return matches ? matches[0] : ''
}
type matchInfo = {
lineStart: number,
character: number,
startIdx: number,
}
// returns the startIdx of the match if there is a match, or undefined if there is no match
// all results are wrt `autocompletion.result`
const getPrefixAutocompletionMatch = ({ prefix, autocompletion }: { prefix: string, autocompletion: Autocompletion }): matchInfo | undefined => {
const trimmedCurrentPrefix = removeLeftTabsAndTrimEnd(prefix)
const trimmedCompletionPrefix = removeLeftTabsAndTrimEnd(autocompletion.prefix)
const trimmedCompletionMiddle = removeLeftTabsAndTrimEnd(autocompletion.insertText)
// console.log('@result: ', JSON.stringify(autocompletion.insertText))
// console.log('@trimmedCurrentPrefix: ', JSON.stringify(trimmedCurrentPrefix))
// console.log('@trimmedCompletionPrefix: ', JSON.stringify(trimmedCompletionPrefix))
// console.log('@trimmedCompletionMiddle: ', JSON.stringify(trimmedCompletionMiddle))
if (trimmedCurrentPrefix.length < trimmedCompletionPrefix.length) { // user must write text beyond the original prefix at generation time
console.log('@undefined1')
return undefined
}
if ( // check that completion starts with the prefix
!(trimmedCompletionPrefix + trimmedCompletionMiddle)
.startsWith(trimmedCurrentPrefix)
) {
console.log('@undefined2')
return undefined
}
// reverse map to find position wrt `autocompletion.result`
const lineStart =
trimmedCurrentPrefix.split('\n').length -
trimmedCompletionPrefix.split('\n').length;
if (lineStart < 0) {
console.log('@undefined3')
console.error('Error: No line found.');
return undefined;
}
const currentPrefixLine = getLastLine(trimmedCurrentPrefix)
const completionPrefixLine = lineStart === 0 ? getLastLine(trimmedCompletionPrefix) : ''
const completionMiddleLine = autocompletion.insertText.split('\n')[lineStart]
const fullCompletionLine = completionPrefixLine + completionMiddleLine
// console.log('currentPrefixLine', currentPrefixLine)
// console.log('completionPrefixLine', completionPrefixLine)
// console.log('completionMiddleLine', completionMiddleLine)
const charMatchIdx = fullCompletionLine.indexOf(currentPrefixLine)
if (charMatchIdx < 0) {
console.log('@undefined4', charMatchIdx)
console.error('Warning: Found character with negative index. This should never happen.')
return undefined
}
const character = (charMatchIdx +
currentPrefixLine.length
- completionPrefixLine.length
)
const startIdx = getIndex(autocompletion.insertText, lineStart, character)
return {
lineStart,
character,
startIdx,
}
}
const getCompletionOptions = ({ prefix, suffix }: { prefix: string, suffix: string }) => {
const prefixLines = prefix.split('\n')
const suffixLines = suffix.split('\n')
const prefixToLeftOfCursor = prefixLines.slice(-1)[0] ?? ''
const suffixToRightOfCursor = suffixLines[0] ?? ''
// default parameters
let shouldGenerate = true
let stopTokens: string[] = ['\n\n', '\r\n\r\n']
// specific cases
if (suffixToRightOfCursor.trim() !== '') { // typing between something
stopTokens = ['\n', '\r\n']
}
// if (prefixToLeftOfCursor.trim() === '' && suffixToRightOfCursor.trim() === '') { // at an empty line
// stopTokens = ['\n\n', '\r\n\r\n']
// }
if (prefixToLeftOfCursor === '') { // at beginning or end of line
shouldGenerate = false
}
return { shouldGenerate, stopTokens }
}
export interface IAutocompleteService {
readonly _serviceBrand: undefined;
}
export const IAutocompleteService = createDecorator<IAutocompleteService>('AutocompleteService');
export class AutocompleteService extends Disposable implements IAutocompleteService {
_serviceBrand: undefined;
private _autocompletionId: number = 0;
private _autocompletionsOfDocument: { [docUriStr: string]: LRUCache<number, Autocompletion> } = {}
private _lastCompletionTime = 0
private _lastPrefix: string = ''
// used internally by vscode
// fires after every keystroke and returns the completion to show
async _provideInlineCompletionItems(
model: ITextModel,
position: Position,
context: InlineCompletionContext,
token: CancellationToken,
): Promise<InlineCompletion[]> {
const disabled = true
const testMode = false
if (disabled) return [];
const docUriStr = model.uri.toString();
const { prefix, suffix } = getPrefixAndSuffix(model, position)
// initialize cache and other variables
// note that whenever an autocompletion is rejected, it is removed from cache
if (!this._autocompletionsOfDocument[docUriStr]) {
this._autocompletionsOfDocument[docUriStr] = new LRUCache<number, Autocompletion>(
MAX_CACHE_SIZE,
(autocompletion: Autocompletion) => {
if (autocompletion.requestId)
this._llmMessageService.abort(autocompletion.requestId)
}
)
}
this._lastPrefix = prefix
// print all pending autocompletions
// let _numPending = 0
// this._autocompletionsOfDocument[docUriStr].items.forEach((a: Autocompletion) => { if (a.status === 'pending') _numPending += 1 })
// console.log('@numPending: ' + _numPending)
// get autocompletion from cache
let cachedAutocompletion: Autocompletion | undefined = undefined
let matchInfo: matchInfo | undefined = undefined
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
// if the user's change matches up with the generated text
matchInfo = getPrefixAutocompletionMatch({ prefix, autocompletion })
if (matchInfo !== undefined) {
cachedAutocompletion = autocompletion
break;
}
}
// if there is a cached autocompletion, return it
if (cachedAutocompletion && matchInfo) {
// console.log('id: ' + cachedAutocompletion.id)
if (cachedAutocompletion.status === 'finished') {
// console.log('A1')
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position, debug: true })
return inlineCompletions
} else if (cachedAutocompletion.status === 'pending') {
// console.log('A2')
try {
await cachedAutocompletion.llmPromise;
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: cachedAutocompletion, prefix, suffix, position })
return inlineCompletions
} catch (e) {
this._autocompletionsOfDocument[docUriStr].delete(cachedAutocompletion.id)
console.error('Error creating autocompletion (1): ' + e)
}
} else if (cachedAutocompletion.status === 'error') {
// console.log('A3')
}
return []
}
// else if no more typing happens, then go forwards with the request
// wait DEBOUNCE_TIME for the user to stop typing
const thisTime = Date.now()
this._lastCompletionTime = thisTime
const didTypingHappenDuringDebounce = await new Promise((resolve, reject) =>
setTimeout(() => {
if (this._lastCompletionTime === thisTime) {
resolve(false)
} else {
resolve(true)
}
}, DEBOUNCE_TIME)
)
// if more typing happened, then do not go forwards with the request
if (didTypingHappenDuringDebounce) {
return []
}
// if there are too many pending requests, cancel the oldest one
let numPending = 0
let oldestPending: Autocompletion | undefined = undefined
for (const autocompletion of this._autocompletionsOfDocument[docUriStr].items.values()) {
if (autocompletion.status === 'pending') {
numPending += 1
if (oldestPending === undefined) {
oldestPending = autocompletion
}
if (numPending >= MAX_PENDING_REQUESTS) {
// cancel the oldest pending request and remove it from cache
this._autocompletionsOfDocument[docUriStr].delete(oldestPending.id)
break
}
}
}
const { shouldGenerate, stopTokens: _ } = getCompletionOptions({ prefix, suffix }) // TODO mat
if (!shouldGenerate) return []
if (testMode && this._autocompletionId !== 0) { // TODO remove this
return []
}
// console.log('B')
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
prefix: prefix,
suffix: suffix,
startTime: Date.now(),
endTime: undefined,
status: 'pending',
llmPromise: undefined,
insertText: '',
requestId: null,
}
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
const requestId = this._llmMessageService.sendLLMMessage({
logging: { loggingName: 'Autocomplete' },
messages: [],
onText: async ({ newText, fullText }) => {
newAutocompletion.insertText = fullText
// if generation doesn't match the prefix for the first few tokens generated, reject it
if (!getPrefixAutocompletionMatch({ prefix: this._lastPrefix, autocompletion: newAutocompletion })) {
reject('LLM response did not match user\'s text.')
}
},
onFinalMessage: ({ fullText }) => {
// newAutocompletion.prefix = prefix
// newAutocompletion.suffix = suffix
// newAutocompletion.startTime = Date.now()
newAutocompletion.endTime = Date.now()
// newAutocompletion.abortRef = { current: () => { } }
newAutocompletion.status = 'finished'
// newAutocompletion.promise = undefined
newAutocompletion.insertText = postprocessResult(extractCodeFromResult(fullText))
resolve(newAutocompletion.insertText)
},
onError: ({ message }) => {
newAutocompletion.endTime = Date.now()
newAutocompletion.status = 'error'
reject(message)
},
featureName: 'Autocomplete',
range: { startLineNumber: position.lineNumber, startColumn: position.column, endLineNumber: position.lineNumber, endColumn: position.column },
})
newAutocompletion.requestId = requestId
// if the request hasnt resolved in TIMEOUT_TIME seconds, reject it
setTimeout(() => {
if (newAutocompletion.status === 'pending') {
reject('Timeout receiving message to LLM.')
}
}, TIMEOUT_TIME)
})
// add autocompletion to cache
this._autocompletionsOfDocument[docUriStr].set(newAutocompletion.id, newAutocompletion)
// show autocompletion
try {
await newAutocompletion.llmPromise
// console.log('id: ' + newAutocompletion.id)
const matchInfo: matchInfo = { startIdx: 0, lineStart: 0, character: 0 }
const inlineCompletions = toInlineCompletions({ matchInfo, autocompletion: newAutocompletion, prefix, suffix, position })
return inlineCompletions
} catch (e) {
this._autocompletionsOfDocument[docUriStr].delete(newAutocompletion.id)
console.error('Error creating autocompletion (2): ' + e)
return []
}
}
constructor(
@ILanguageFeaturesService private _langFeatureService: ILanguageFeaturesService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
) {
super()
this._langFeatureService.inlineCompletionsProvider.register('*', {
provideInlineCompletions: async (model, position, context, token) => {
const items = await this._provideInlineCompletionItems(model, position, context, token)
// console.log('item: ', items?.[0]?.insertText)
return { items: items, }
},
freeInlineCompletions: (completions) => {
// get the `docUriStr` and the `position` of the cursor
const activePane = this._editorService.activeEditorPane;
if (!activePane) return;
const control = activePane.getControl();
if (!control || !isCodeEditor(control)) return;
const position = control.getPosition();
if (!position) return;
const resource = EditorResourceAccessor.getCanonicalUri(this._editorService.activeEditor);
if (!resource) return;
const model = this._modelService.getModel(resource)
if (!model) return;
const docUriStr = resource.toString();
const { prefix, } = getPrefixAndSuffix(model, position)
if (!this._autocompletionsOfDocument[docUriStr]) return;
// go through cached items and remove matching ones
// autocompletion.prefix + autocompletion.insertedText ~== insertedText
completions.items.forEach(item => {
this._autocompletionsOfDocument[docUriStr].items.forEach((autocompletion: Autocompletion) => {
if (removeLeftTabsAndTrimEnd(prefix)
=== removeLeftTabsAndTrimEnd(autocompletion.prefix + autocompletion.insertText)
) {
this._autocompletionsOfDocument[docUriStr].delete(autocompletion.id);
}
});
});
},
})
}
}
registerSingleton(IAutocompleteService, AutocompleteService, InstantiationType.Eager);
@@ -0,0 +1,178 @@
import { Disposable } from '../../../../../base/common/lifecycle.js';
import { URI } from '../../../../../base/common/uri.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
import { ICodeEditor } from '../../../../../editor/browser/editorBrowser.js';
import { ICodeEditorService } from '../../../../../editor/browser/services/codeEditorService.js';
import { InstantiationType, registerSingleton } from '../../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../../platform/instantiation/common/instantiation.js';
// lets you add a "consistent" item to a Model (aka URI), instead of just to a single editor
type AddItemInputs = { uri: URI; fn: (editor: ICodeEditor) => (() => void); }
export interface IConsistentItemService {
readonly _serviceBrand: undefined;
getEditorsOnURI(uri: URI): ICodeEditor[];
addConsistentItemToURI(inputs: AddItemInputs): string;
removeConsistentItemFromURI(consistentItemId: string): void;
}
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
export class ConsistentItemService extends Disposable {
readonly _serviceBrand: undefined
// the items that are attached to each URI, completely independent from current state of editors
private readonly consistentItemIdsOfURI: Record<string, Set<string> | undefined> = {}
private readonly infoOfConsistentItemId: Record<string, AddItemInputs> = {}
// current state of items on each editor, and the fns to call to remove them
private readonly itemIdsOfEditorId: Record<string, Set<string> | undefined> = {}
private readonly consistentItemIdOfItemId: Record<string, string> = {}
private readonly disposeFnOfItemId: Record<string, () => void> = {}
constructor(
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
const removeItemsFromEditor = (editor: ICodeEditor) => {
const editorId = editor.getId()
for (const itemId of this.itemIdsOfEditorId[editorId] ?? [])
this._removeItemFromEditor(editor, itemId)
}
// put items on the editor, based on the consistent items for that URI
const putItemsOnEditor = (editor: ICodeEditor, uri: URI | null) => {
if (!uri) return
for (const consistentItemId of this.consistentItemIdsOfURI[uri.fsPath] ?? [])
this._putItemOnEditor(editor, consistentItemId)
}
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(
editor.onDidChangeModel(e => {
removeItemsFromEditor(editor)
putItemsOnEditor(editor, e.newModelUrl)
})
)
}
const addDisposeListener = (editor: ICodeEditor) => {
this._register(editor.onDidDispose(() => {
// anything on the editor has been disposed already
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? [])
delete this.disposeFnOfItemId[itemId]
}))
}
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
addDisposeListener(editor)
putItemsOnEditor(editor, editor.getModel()?.uri ?? null)
}
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// when an editor is deleted, remove its items
this._register(this._editorService.onCodeEditorRemove(editor => {
removeItemsFromEditor(editor)
}))
}
_putItemOnEditor(editor: ICodeEditor, consistentItemId: string) {
const { fn } = this.infoOfConsistentItemId[consistentItemId]
// add item
const dispose = fn(editor)
const itemId = generateUuid()
const editorId = editor.getId()
if (!(editorId in this.itemIdsOfEditorId))
this.itemIdsOfEditorId[editorId] = new Set()
this.itemIdsOfEditorId[editorId]!.add(itemId)
this.consistentItemIdOfItemId[itemId] = consistentItemId
this.disposeFnOfItemId[itemId] = () => {
// console.log('calling remove for', itemId)
dispose?.()
}
}
_removeItemFromEditor(editor: ICodeEditor, itemId: string) {
const editorId = editor.getId()
this.itemIdsOfEditorId[editorId]?.delete(itemId)
this.disposeFnOfItemId[itemId]?.()
delete this.disposeFnOfItemId[itemId]
delete this.consistentItemIdOfItemId[itemId]
}
getEditorsOnURI(uri: URI) {
const editors = this._editorService.listCodeEditors().filter(editor => editor.getModel()?.uri.fsPath === uri.fsPath)
return editors
}
consistentItemIdPool = 0
addConsistentItemToURI({ uri, fn }: AddItemInputs) {
const consistentItemId = (this.consistentItemIdPool++) + ''
if (!(uri.fsPath in this.consistentItemIdsOfURI))
this.consistentItemIdsOfURI[uri.fsPath] = new Set()
this.consistentItemIdsOfURI[uri.fsPath]!.add(consistentItemId)
this.infoOfConsistentItemId[consistentItemId] = { fn, uri }
const editors = this.getEditorsOnURI(uri)
for (const editor of editors)
this._putItemOnEditor(editor, consistentItemId)
return consistentItemId
}
removeConsistentItemFromURI(consistentItemId: string) {
if (!(consistentItemId in this.infoOfConsistentItemId))
return
const { uri } = this.infoOfConsistentItemId[consistentItemId]
const editors = this.getEditorsOnURI(uri)
for (const editor of editors) {
for (const itemId of this.itemIdsOfEditorId[editor.getId()] ?? []) {
if (this.consistentItemIdOfItemId[itemId] === consistentItemId)
this._removeItemFromEditor(editor, itemId)
}
}
// clear
this.consistentItemIdsOfURI[uri.fsPath]?.delete(consistentItemId)
delete this.infoOfConsistentItemId[consistentItemId]
}
}
registerSingleton(IConsistentItemService, ConsistentItemService, InstantiationType.Eager);
@@ -0,0 +1,276 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { diffLines } from '../react/out/diff/index.js'
export type ComputedDiff = {
type: 'edit';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
code: string;
startLine: number; // 1-indexed
endLine: number;
} | {
type: 'insertion';
// originalCode: string;
originalStartLine: number; // insertion starts on column 0 of this
// originalEndLine: number;
code: string;
startLine: number;
endLine: number;
} | {
type: 'deletion';
originalCode: string;
originalStartLine: number;
originalEndLine: number;
// code: string;
startLine: number; // deletion starts on column 0 of this
// endLine: number;
}
export function findDiffs(oldStr: string, newStr: string) {
// this makes it so the end of the file always ends with a \n (if you don't have this, then diffing E vs E\n gives an "edit". With it, you end up diffing E\n vs E\n\n which now properly gives an insertion)
newStr += '\n';
oldStr += '\n';
// an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
const lineByLineChanges = diffLines(oldStr, newStr);
lineByLineChanges.push({ value: '', added: false, removed: false }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
let oldFileLineNum: number = 1;
let newFileLineNum: number = 1;
let streakStartInNewFile: number | undefined = undefined
let streakStartInOldFile: number | undefined = undefined
const oldStrLines = ('\n' + oldStr).split('\n') // add newline so indexing starts at 1
const newStrLines = ('\n' + newStr).split('\n')
const replacements: ComputedDiff[] = []
for (const line of lineByLineChanges) {
// no change on this line
if (!line.added && !line.removed) {
// do nothing
// if we were on a streak of +s and -s, end it
if (streakStartInNewFile !== undefined) {
let type: 'edit' | 'insertion' | 'deletion' = 'edit'
const startLine = streakStartInNewFile
const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
const originalStartLine = streakStartInOldFile!
const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
// if the range is empty, mark it as a deletion / insertion (both won't be true at once)
// DELETION
if (endLine === startLine - 1) {
type = 'deletion'
// endLine = startLine
}
// INSERTION
else if (originalEndLine === originalStartLine - 1) {
type = 'insertion'
// originalEndLine = originalStartLine
}
const replacement: ComputedDiff = {
type,
startLine, endLine,
// startCol, endCol,
originalStartLine, originalEndLine,
// code: newContent,
// originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol),
originalCode: originalContent,
code: newContent,
}
replacements.push(replacement)
streakStartInNewFile = undefined
streakStartInOldFile = undefined
}
oldFileLineNum += line.count ?? 0;
newFileLineNum += line.count ?? 0;
}
// line was removed from old file
else if (line.removed) {
// if we weren't on a streak, start one on this current line num
if (streakStartInNewFile === undefined) {
streakStartInNewFile = newFileLineNum
streakStartInOldFile = oldFileLineNum
}
oldFileLineNum += line.count ?? 0 // we processed the line so add 1 (or "count")
}
// line was added to new file
else if (line.added) {
// if we weren't on a streak, start one on this current line num
if (streakStartInNewFile === undefined) {
streakStartInNewFile = newFileLineNum
streakStartInOldFile = oldFileLineNum
}
newFileLineNum += line.count ?? 0; // we processed the line so add 1 (or "count")
}
} // end for
// console.log('DIFF', { oldStr, newStr, replacements })
return replacements
}
// // uncomment this to test
// let name_ = ''
// let testsFailed = 0
// const assertEqual = (a: { [s: string]: any }, b: { [s: string]: any }) => {
// let keys = new Set([...Object.keys(a), ...Object.keys(b)])
// for (let k of keys) {
// if (a[k] !== b[k]) {
// console.error('Void Test Error:', name_, '\n', `${k}=`, `${JSON.stringify(a[k])}, ${JSON.stringify(b[k])}`)
// // console.error(JSON.stringify(a, null, 4))
// // console.error(JSON.stringify(b, null, 4))
// testsFailed += 1
// }
// }
// }
// const test = (name: string, fn: () => void) => {
// name_ = name
// fn()
// }
// const originalCode = `\
// A
// B
// C
// D
// E`
// const insertedCode = `\
// A
// B
// C
// F
// D
// E`
// const modifiedCode = `\
// A
// B
// C
// F
// E`
// const modifiedCode2 = `\
// A
// B
// C
// D
// E
// `
// test('Diffs Insertion', () => {
// const diffs = findDiffs(originalCode, insertedCode)
// const expected: BaseDiff = {
// type: 'insertion',
// originalCode: '',
// originalStartLine: 4, // empty range where the insertion happened
// originalEndLine: 4,
// startLine: 4,
// startCol: 1,
// endLine: 4,
// endCol: Number.MAX_SAFE_INTEGER,
// }
// assertEqual(diffs[0], expected)
// })
// test('Diffs Deletion', () => {
// const diffs = findDiffs(insertedCode, originalCode)
// assertEqual({ length: diffs.length }, { length: 1 })
// const expected: BaseDiff = {
// type: 'deletion',
// originalCode: 'F',
// originalStartLine: 4,
// originalEndLine: 4,
// startLine: 4,
// startCol: 1, // empty range where the deletion happened
// endLine: 4,
// endCol: 1,
// }
// assertEqual(diffs[0], expected)
// })
// test('Diffs Modification', () => {
// const diffs = findDiffs(originalCode, modifiedCode)
// assertEqual({ length: diffs.length }, { length: 1 })
// const expected: BaseDiff = {
// type: 'edit',
// originalCode: 'D',
// originalStartLine: 4,
// originalEndLine: 4,
// startLine: 4,
// startCol: 1,
// endLine: 4,
// endCol: Number.MAX_SAFE_INTEGER,
// }
// assertEqual(diffs[0], expected)
// })
// test('Diffs Modification 2', () => {
// const diffs = findDiffs(originalCode, modifiedCode2)
// assertEqual({ length: diffs.length }, { length: 1 })
// const expected: BaseDiff = {
// type: 'insertion',
// originalCode: '',
// originalStartLine: 6,
// originalEndLine: 6,
// startLine: 6,
// startCol: 1,
// endLine: 6,
// endCol: Number.MAX_SAFE_INTEGER,
// }
// assertEqual(diffs[0], expected)
// })
// if (testsFailed === 0) {
// console.log('✅ Void - All tests passed')
// }
// else {
// console.log('❌ Void - At least one test failed')
// }
@@ -0,0 +1,20 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { isMacintosh } from '../../../../../base/common/platform.js';
// import { OperatingSystem, OS } from '../../../../base/common/platform.js';
// OS === OperatingSystem.Macintosh
export function getCmdKey(): string {
if (isMacintosh) {
return '⌘';
} else {
return 'Ctrl';
}
}
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,24 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
.monaco-editor .void-sweepIdxBG {
background-color: var(--vscode-void-sweepIdxBG);
}
.void-sweepBG {
background-color: var(--vscode-void-sweepBG);
}
.void-highlightBG {
background-color: var(--vscode-void-highlightBG);
}
.void-greenBG {
background-color: var(--vscode-void-greenBG);
}
.void-redBG {
background-color: var(--vscode-void-redBG);
}
@@ -0,0 +1,641 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { CodeSelection } from '../threadHistoryService.js';
export const chat_systemMessage = `\
You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
Instructions:
1. Output the changes to make to the entire file.
1. Do not re-write the entire file.
3. Instead, you may use code elision to represent unchanged portions of code. For example, write "existing code..." in code comments.
4. You must give enough context to apply the change in the correct location.
## EXAMPLE
FILES
selected file \`math.ts\`:
\`\`\`
const addNumbers = (a, b) => a + b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
\`\`\`
SELECTION
\`\`\`
const subtractNumbers = (a, b) => a - b
\`\`\`
INSTRUCTIONS
\`\`\`
add a function that multiplies numbers below this
\`\`\`
EXPECTED OUTPUT
We can add the following code to the file:
\`\`\`
// existing code...
const subtractNumbers = (a, b) => a - b;
const multiplyNumbers = (a, b) => a * b;
// existing code...
\`\`\`
## EXAMPLE
FILES
selected file \`fib.ts\`:
\`\`\`
const dfs = (root) => {
if (!root) return;
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
\`\`\`
SELECTION
\`\`\`
return fib(n - 1) + fib(n - 2)
\`\`\`
INSTRUCTIONS
\`\`\`
memoize results
\`\`\`
EXPECTED OUTPUT
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
\`\`\`
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
if (memo[n]) return memo[n]; // Check if result is already computed
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
\`\`\`
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
Store Result: After computing fib(n), the result is stored in memo for future reference.
## END EXAMPLES\
`
const stringifySelections = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\`
${content // this was the enite file which is foolish
}
\`\`\`${selectionStr === null ? '' : `
Selection: ${selectionStr}`}
`).join('\n')
}
export const chat_prompt = (instructions: string, selections: CodeSelection[] | null) => {
let str = '';
if (selections && selections.length > 0) {
str += stringifySelections(selections);
str += `Please edit the selected code following these instructions:\n`
}
str += `${instructions}`;
return str;
};
export const ctrlLStream_systemMessage = `
You are a coding assistant that applies a diff to a file. You are given the original file \`original_file\`, a diff \`diff\`, and a new file that you are applying the diff to \`new_file\`.
Please finish writing the new file \`new_file\`, according to the diff \`diff\`. You must completely re-write the whole file, using the diff.
Directions:
1. Continue exactly where the new file \`new_file\` left off.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. Note that \`+\` lines represent additions, \`-\` lines represent removals, and space lines \` \` represent no change.
# Example 1:
ORIGINAL_FILE
\`Sidebar.tsx\`:
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<button
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</button>
</li>
))}
</ul>
<button className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</button>
</div>
);
};
export default Sidebar;
\`\`\`
DIFF
\`\`\`
@@ ... @@
-<div className={styles.sidebar}>
-<ul>
- {items.map((item, index) => (
- <li key={index}>
- <button
- className={styles.sidebarButton}
- onClick={() => onItemSelect?.(item.label)}
- >
- {item.label}
- </button>
- </li>
- ))}
-</ul>
-<button className={styles.extraButton} onClick={onExtraButtonClick}>
- Extra Action
-</button>
-</div>
+<div className={styles.sidebar}>
+<ul>
+ {items.map((item, index) => (
+ <li key={index}>
+ <div
+ className={styles.sidebarButton}
+ onClick={() => onItemSelect?.(item.label)}
+ >
+ {item.label}
+ </div>
+ </li>
+ ))}
+</ul>
+<div className={styles.extraButton} onClick={onExtraButtonClick}>
+ Extra Action
+</div>
+</div>
\`\`\`
NEW_FILE
\`\`\`
import React from 'react';
import styles from './Sidebar.module.css';
interface SidebarProps {
items: { label: string; href: string }[];
onItemSelect?: (label: string) => void;
onExtraButtonClick?: () => void;
}
const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
return (
\`\`\`
COMPLETION
\`\`\`
<div className={styles.sidebar}>
<ul>
{items.map((item, index) => (
<li key={index}>
<div
className={styles.sidebarButton}
onClick={() => onItemSelect?.(item.label)}
>
{item.label}
</div>
</li>
))}
</ul>
<div className={styles.extraButton} onClick={onExtraButtonClick}>
Extra Action
</div>
</div>
);
};
export default Sidebar;\`\`\`
`
export const ctrlLStream_prompt = ({ originalCode, userMessage }: { originalCode: string, userMessage: string }) => {
return `\
ORIGINAL_CODE
\`\`\`
${originalCode}
\`\`\`
DIFF
\`\`\`
${userMessage}
\`\`\`
INSTRUCTIONS
Please finish writing the new file by applying the diff to the original file. Return ONLY the completion of the file, without any explanation.
`
}
export const ctrlKStream_systemMessage = `\
`
export const ctrlKStream_prefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_CHARS = 1024
/*
a
a
a <-- final i (prefix = a\na\n)
a
|b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
c
d| <-- endLine-1 <-- initial j (moves down)
e
e <-- final j (suffix = e\ne\n)
e
e
*/
let prefix = ''
let i = startLine - 1 // 0-indexed exclusive
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
while (i !== 0) {
const newLine = fullFileLines[i - 1]
if (newLine.length + 1 + prefix.length <= MAX_CHARS) { // +1 to include the \n
prefix = `${newLine}\n${prefix}`
i -= 1
}
else break
}
let suffix = ''
let j = endLine - 1
while (j !== fullFileLines.length - 1) {
const newLine = fullFileLines[j + 1]
if (newLine.length + 1 + suffix.length <= MAX_CHARS) { // +1 to include the \n
suffix = `${suffix}\n${newLine}`
j += 1
}
else break
}
return { prefix, suffix }
}
export const ctrlKStream_prompt = ({ selection, prefix, suffix, userMessage }: { selection: string, prefix: string, suffix: string, userMessage: string, }) => {
const onlySpeaksFIM = false
if (onlySpeaksFIM) {
const preTag = 'PRE'
const sufTag = 'SUF'
const midTag = 'MID'
return `\
<${preTag}>
/* Original Selection:
${selection}*/
/* Instructions: ${userMessage}*/
${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
<${midTag}>`
}
// prompt the model on how to do FIM
else {
const preTag = 'PRE'
const sufTag = 'SUF'
const midTag = 'MID'
return `\
Here is the user's original selection:
\`\`\`
<${midTag}>${selection}</${midTag}>
\`\`\`
The user wants to apply the following instructions to the selection:
${userMessage}
Please rewrite the selection following the user's instructions.
Instructions to follow:
1. Follow the user's instructions
2. You may ONLY CHANGE the selection, and nothing else in the file
3. Make sure all brackets in the new selection are balanced the same was as in the original selection
3. Be careful not to duplicate or remove variables, comments, or other syntax by mistake
Complete the following:
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
<${midTag}>`
}
};
// export const searchDiffChunkInstructions = `
// You are a coding assistant that applies a diff to a file. You are given a diff \`diff\`, a list of files \`files\` to apply the diff to, and a selection \`selection\` that you are currently considering in the file.
// Determine whether you should modify ANY PART of the selection \`selection\` following the \`diff\`. Return \`true\` if you should modify any part of the selection, and \`false\` if you should not modify any part of it.
// # Example 1:
// FILES
// selected file \`Sidebar.tsx\`:
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// <li key={index}>
// <button
// className={styles.sidebarButton}
// onClick={() => onItemSelect?.(item.label)}
// >
// {item.label}
// </button>
// </li>
// ))}
// </ul>
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
// Extra Action
// </button>
// </div>
// );
// };
// export default Sidebar;
// \`\`\`
// DIFF
// \`\`\`
// @@ ... @@
// -<div className={styles.sidebar}>
// -<ul>
// - {items.map((item, index) => (
// - <li key={index}>
// - <button
// - className={styles.sidebarButton}
// - onClick={() => onItemSelect?.(item.label)}
// - >
// - {item.label}
// - </button>
// - </li>
// - ))}
// -</ul>
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
// - Extra Action
// -</button>
// -</div>
// +<div className={styles.sidebar}>
// +<ul>
// + {items.map((item, index) => (
// + <li key={index}>
// + <div
// + className={styles.sidebarButton}
// + onClick={() => onItemSelect?.(item.label)}
// + >
// + {item.label}
// + </div>
// + </li>
// + ))}
// +</ul>
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
// + Extra Action
// +</div>
// +</div>
// \`\`\`
// SELECTION
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// \`\`\`
// RESULT
// The output should be \`true\` because the diff begins on the line with \`<div className={styles.sidebar}>\` and this line is present in the selection.
// OUTPUT
// \`true\`
// `
// export const generateDiffInstructions = `
// You are a coding assistant. You are given a list of relevant files \`files\`, a selection that the user is making \`selection\`, and instructions to follow \`instructions\`.
// Please edit the selected file following the user's instructions (or, if appropriate, answer their question instead).
// All changes made to files must be outputted in unified diff format.
// Unified diff format instructions:
// 1. Each diff must begin with \`\`\`@@ ... @@\`\`\`.
// 2. Each line must start with a \`+\` or \`-\` or \` \` symbol.
// 3. Make diffs more than a few lines.
// 4. Make high-level diffs rather than many one-line diffs.
// Here's an example of unified diff format:
// \`\`\`
// @@ ... @@
// -def factorial(n):
// - if n == 0:
// - return 1
// - else:
// - return n * factorial(n-1)
// +def factorial(number):
// + if number == 0:
// + return 1
// + else:
// + return number * factorial(number-1)
// \`\`\`
// Please create high-level diffs where you group edits together if they are near each other, like in the above example. Another way to represent the above example is to make many small line edits. However, this is less preferred, because the edits are not high-level. The edits are close together and should be grouped:
// \`\`\`
// @@ ... @@ # This is less preferred because edits are close together and should be grouped:
// -def factorial(n):
// +def factorial(number):
// - if n == 0:
// + if number == 0:
// return 1
// else:
// - return n * factorial(n-1)
// + return number * factorial(number-1)
// \`\`\`
// # Example 1:
// FILES
// selected file \`test.ts\`:
// \`\`\`
// x = 1
// {{selection}}
// z = 3
// \`\`\`
// SELECTION
// \`\`\`const y = 2\`\`\`
// INSTRUCTIONS
// \`\`\`y = 3\`\`\`
// EXPECTED RESULT
// We should change the selection from \`\`\`y = 2\`\`\` to \`\`\`y = 3\`\`\`.
// \`\`\`
// @@ ... @@
// -x = 1
// -
// -y = 2
// +x = 1
// +
// +y = 3
// \`\`\`
// # Example 2:
// FILES
// selected file \`Sidebar.tsx\`:
// \`\`\`
// import React from 'react';
// import styles from './Sidebar.module.css';
// interface SidebarProps {
// items: { label: string; href: string }[];
// onItemSelect?: (label: string) => void;
// onExtraButtonClick?: () => void;
// }
// const Sidebar: React.FC<SidebarProps> = ({ items, onItemSelect, onExtraButtonClick }) => {
// return (
// <div className={styles.sidebar}>
// <ul>
// {items.map((item, index) => (
// <li key={index}>
// {{selection}}
// className={styles.sidebarButton}
// onClick={() => onItemSelect?.(item.label)}
// >
// {item.label}
// </button>
// </li>
// ))}
// </ul>
// <button className={styles.extraButton} onClick={onExtraButtonClick}>
// Extra Action
// </button>
// </div>
// );
// };
// export default Sidebar;
// \`\`\`
// SELECTION
// \`\`\` <button\`\`\`
// INSTRUCTIONS
// \`\`\`make all the buttons like this into divs\`\`\`
// EXPECTED OUTPUT
// We should change all the buttons like the one selected into a div component. Here is the change:
// \`\`\`
// @@ ... @@
// -<div className={styles.sidebar}>
// -<ul>
// - {items.map((item, index) => (
// - <li key={index}>
// - <button
// - className={styles.sidebarButton}
// - onClick={() => onItemSelect?.(item.label)}
// - >
// - {item.label}
// - </button>
// - </li>
// - ))}
// -</ul>
// -<button className={styles.extraButton} onClick={onExtraButtonClick}>
// - Extra Action
// -</button>
// -</div>
// +<div className={styles.sidebar}>
// +<ul>
// + {items.map((item, index) => (
// + <li key={index}>
// + <div
// + className={styles.sidebarButton}
// + onClick={() => onItemSelect?.(item.label)}
// + >
// + {item.label}
// + </div>
// + </li>
// + ))}
// +</ul>
// +<div className={styles.extraButton} onClick={onExtraButtonClick}>
// + Extra Action
// +</div>
// +</div>
// \`\`\`
// `;
@@ -0,0 +1,64 @@
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { IMetricsService } from '../../../../platform/void/common/metricsService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IInlineDiffsService } from './inlineDiffsService.js';
import { InputBox } from '../../../../base/browser/ui/inputbox/inputBox.js';
export type QuickEditPropsType = {
diffareaid: number,
onGetInputBox: (i: InputBox) => void;
onChangeHeight: (height: number) => void;
onUserUpdateText: (text: string) => void;
initText: string | null;
}
export type QuickEdit = {
startLine: number, // 0-indexed
beforeCode: string,
afterCode?: string,
instructions?: string,
responseText?: string, // model can produce a text response too
}
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
registerAction2(class extends Action2 {
constructor(
) {
super({
id: VOID_CTRL_K_ACTION_ID,
title: 'Void: Quick Edit',
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
weight: KeybindingWeight.BuiltinExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editorService = accessor.get(ICodeEditorService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('User Action', { type: 'Open Ctrl+K' })
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
const model = editor.getModel()
if (!model) return;
const selection = editor.getSelection()
if (!selection) return;
const { startLineNumber: startLine, endLineNumber: endLine } = selection
// deselect - clear selection
editor.setSelection({ startLineNumber: startLine, endLineNumber: startLine, startColumn: 1, endColumn: 1 })
const inlineDiffsService = accessor.get(IInlineDiffsService)
inlineDiffsService.addCtrlKZone({ startLine, endLine, editor })
}
});
@@ -0,0 +1,72 @@
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { QuickEdit } from './quickEditActions.js';
// service that manages state
export type VoidQuickEditState = {
quickEditsOfDocument: { [uri: string]: QuickEdit }
}
export interface IQuickEditStateService {
readonly _serviceBrand: undefined;
readonly state: VoidQuickEditState; // readonly to the user
setState(newState: Partial<VoidQuickEditState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
}
export const IQuickEditStateService = createDecorator<IQuickEditStateService>('voidQuickEditStateService');
class VoidQuickEditStateService extends Disposable implements IQuickEditStateService {
_serviceBrand: undefined;
static readonly ID = 'voidQuickEditStateService';
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: VoidQuickEditState
constructor(
) {
super()
// initial state
this.state = { quickEditsOfDocument: {} }
}
setState(newState: Partial<VoidQuickEditState>) {
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
fireFocusChat() {
this._onFocusChat.fire()
}
fireBlurChat() {
this._onBlurChat.fire()
}
}
registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);
@@ -0,0 +1,2 @@
out/
src2/
@@ -0,0 +1,10 @@
Run `node build.js` to compile the React into `out/`.
A couple things to remember:
- Make sure to add .js at the end of any external imports used in here, e.g. ../../../../../my_file.js. If you don't do this, you will get untraceable errors.
- src/ needs to be shallow (1 folder deep) so the detection of externals works properly (see tsup.config.js).
+65
View File
@@ -0,0 +1,65 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { spawn, execSync } from 'child_process';
const args = process.argv.slice(2);
const isWatch = args.includes('--watch') || args.includes('-w');
if (isWatch) {
// Watch mode
// Create a watcher for scope-tailwind using nodemon
const scopeTailwindWatcher = spawn('npx', [
'nodemon',
'--watch', 'src',
'--ext', 'ts,tsx,css',
'--exec',
'npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"'
]);
// Create a watcher for tsup in watch mode
const tsupWatcher = spawn('npx', [
'tsup',
'--watch'
]);
// Handle scope-tailwind watcher output
scopeTailwindWatcher.stdout.on('data', (data) => {
console.log(`[scope-tailwind] ${data}`);
});
scopeTailwindWatcher.stderr.on('data', (data) => {
console.error(`[scope-tailwind] ${data}`);
});
// Handle tsup watcher output
tsupWatcher.stdout.on('data', (data) => {
console.log(`[tsup] ${data}`);
});
tsupWatcher.stderr.on('data', (data) => {
console.error(`[tsup] ${data}`);
});
// Handle process termination
process.on('SIGINT', () => {
scopeTailwindWatcher.kill();
tsupWatcher.kill();
process.exit();
});
console.log('🔄 Watchers started! Press Ctrl+C to stop both watchers.');
} else {
// Build mode
console.log('📦 Building...');
// Run scope-tailwind once
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "void-"', { stdio: 'inherit' });
// Run tsup once
execSync('npx tsup', { stdio: 'inherit' });
console.log('✅ Build complete!');
}
@@ -0,0 +1,23 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { useEffect, useState } from 'react'
import { useIsDark, useSidebarState } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { CtrlKChat } from './CtrlKChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
export const CtrlK = (props: QuickEditPropsType) => {
const isDark = useIsDark()
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<ErrorBoundary>
<CtrlKChat {...props} />
</ErrorBoundary>
</div>
}
@@ -0,0 +1,153 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useThreadsState, useQuickEditState, useAccessor } from '../util/services.js';
import { OnError } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { getCmdKey } from '../../../helpers/getCmdKey.js';
import { VoidInputBox } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit } from '../sidebar-tsx/SidebarChat.js';
export const CtrlKChat = ({ diffareaid, onGetInputBox, onUserUpdateText, onChangeHeight, initText }: QuickEditPropsType) => {
const accessor = useAccessor()
const inlineDiffsService = accessor.get('IInlineDiffsService')
const sizerRef = useRef<HTMLDivElement | null>(null)
const inputBoxRef: React.MutableRefObject<InputBox | null> = useRef(null);
useEffect(() => {
const inputContainer = sizerRef.current
if (!inputContainer) return;
// only observing 1 element
let resizeObserver: ResizeObserver | undefined
resizeObserver = new ResizeObserver((entries) => {
const height = entries[0].borderBoxSize[0].blockSize
onChangeHeight(height)
})
resizeObserver.observe(inputContainer);
return () => { resizeObserver?.disconnect(); };
}, [onChangeHeight]);
// state of current message
const [instructions, setInstructions] = useState(initText ?? '') // the user's instructions
const onChangeText = useCallback((newStr: string) => {
setInstructions(newStr)
onUserUpdateText(newStr)
}, [setInstructions])
const isDisabled = !instructions.trim()
const currentlyStreamingIdRef = useRef<number | undefined>(undefined)
const [isStreaming, setIsStreaming] = useState(false)
const onSubmit = useCallback((e: FormEvent) => {
if (currentlyStreamingIdRef.current !== undefined) return
inputBoxRef.current?.disable()
currentlyStreamingIdRef.current = inlineDiffsService.startApplying({
featureName: 'Ctrl+K',
diffareaid: diffareaid,
userMessage: instructions,
})
setIsStreaming(true)
}, [inlineDiffsService, diffareaid, instructions])
const onInterrupt = useCallback(() => {
if (currentlyStreamingIdRef.current !== undefined)
inlineDiffsService.interruptStreaming(currentlyStreamingIdRef.current)
setIsStreaming(false)
}, [inlineDiffsService])
// sync init value
const alreadySetRef = useRef(false)
useEffect(() => {
if (!inputBoxRef.current) return
if (alreadySetRef.current) return
alreadySetRef.current = true
inputBoxRef.current.value = instructions
}, [initText, instructions])
return <div className='py-2 w-full max-w-xl' ref={sizerRef}>
<form
// copied from SidebarChat.tsx
className={`
flex flex-col gap-2 p-1 relative input text-left shrink-0
transition-all duration-200
rounded-md
bg-vscode-input-bg
border border-vscode-commandcenter-inactive-border focus-within:border-vscode-commandcenter-active-border hover:border-vscode-commandcenter-active-border
`
}
onKeyDown={(e) => {
if (e.key === 'Enter' && !e.shiftKey) {
onSubmit(e)
return
}
}}
onSubmit={(e) => {
if (isDisabled) {
// __TODO__ show disabled
return
}
console.log('submit!')
onSubmit(e)
}}
onClick={(e) => {
if (e.currentTarget === e.target) {
inputBoxRef.current?.focus()
}
}}
>
<div // this div is used to position the input box properly
className={`w-full p-2 z-[999]`}
>
<div className='flex flex-row justify-between items-end gap-1'>
{/* left (input) */}
<div // copied from SidebarChat.tsx
className={`w-full
@@[&_textarea]:!void-bg-transparent @@[&_textarea]:!void-outline-none @@[&_textarea]:!void-text-vscode-input-fg @@[&_div.monaco-inputbox]:!void-outline-none`}>
{/* text input */}
<VoidInputBox
placeholder={`${getCmdKey()}+K to select`}
onChangeText={onChangeText}
onCreateInstance={useCallback((instance: InputBox) => {
inputBoxRef.current = instance;
onGetInputBox(instance);
instance.focus()
}, [onGetInputBox])}
multiline={true}
/>
</div>
{/* right (button) */}
<div className='flex flex-row items-end'>
{/* submit / stop button */}
{isStreaming ?
// stop button
<ButtonStop
onClick={onInterrupt}
/>
:
// submit button (up arrow)
<ButtonSubmit
disabled={isDisabled}
/>
}
</div>
</div>
</div>
</form>
</div>
}
@@ -0,0 +1,12 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { CtrlK } from './CtrlK.js'
export const mountCtrlK = mountFnGenerator(CtrlK)
@@ -0,0 +1,8 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPL 3.0 License.
*--------------------------------------------------------------------------------------------*/
import { diffLines, Change } from 'diff';
export { diffLines, Change }

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