Compare commits

...

201 Commits

Author SHA1 Message Date
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
Andrew Pareles 4ba00f6216 scope-tailwind doc 2024-12-06 02:55:28 -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
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
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
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
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
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
114 changed files with 8274 additions and 13285 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.
+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";
};
};
};
};
}
+20 -58
View File
@@ -9,39 +9,11 @@ There are a few ways to contribute:
- Submit Issues/Docs/Bugs ([Issues](https://github.com/voideditor/void/issues))
## 1. Building the Extension
## Building the full IDE
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
Please follow the steps below to build the IDE. If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new) with any build errors, or refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
1. Clone the repository:
```
git clone https://github.com/voideditor/void
```
2. Open the folder `/extensions/void` in VSCode (open it in a new workspace, _don't_ just cd into it).
3. Install dependencies:
```
npm install
```
1. Compile the React files by running `npm run build`. This build command converts all the Tailwind/React entrypoint files into raw .css and .js files in `dist/`.
```
npm run build
```
5. Run the extension in a new window by pressing <kbd>F5</kbd>.
This will start a new instance of VSCode with the extension enabled. If this doesn't work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VSCode Extension Development".
## 2. Building the full IDE
If you want to work on the full IDE, please follow the steps below. If you have any questions/issues, you can refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page. Also feel free to submit an issue or get in touch with us with any build errors.
<!-- TODO say whether you can build each distribution on any Operating System, or if you need to build Windows on Windows, etc -->
Most of Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
### a. Build Prerequisites - Mac
@@ -51,27 +23,27 @@ If you're using a Mac, make sure you have Python and XCode installed (you probab
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.
Find the box for Visual Studio 2022 (or VS Build Tools) and click Install/Modify.
Under Workloads, select "Desktop development with C++" and "Node.js build tools".
Under Individual components, select every item under:
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/Modify.
Finally, click Install.
### c. Build Prerequisites - Linux
We haven't created prerequisite steps for building on Linux yet, but you can follow [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
First, make sure you've installed NodeJS and 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).
### Build instructions
Before building Void, please follow the prerequisite steps above for your operating system. Also make sure you've already built the Void extension (or just run `cd ./extensions/void && npm install && npm run build && npm run compile && cd ../..`).
To build Void, first open `void/` in VSCode. Then:
To build Void, first follow the prerequisite steps above for your operating system and open `void/` inside VSCode. Then:
1. Install all dependencies.
@@ -79,7 +51,9 @@ To build Void, first open `void/` in VSCode. Then:
npm install
```
2. Press <kbd>Ctrl+Shift+B</kbd>, or if you prefer using the terminal run `npm run watch`.
2. Run `cd ./src/vs/workbench/contrib/void/browser/react/` and then `node ./build.js` to build Void's external dependencies (our React components, etc).
3. Press <kbd>Ctrl+Shift+B</kbd>, or if you prefer using the terminal run `npm run watch`.
This can take ~5 min.
@@ -96,11 +70,10 @@ If you ran `npm run watch`, the build is done when you see something like this:
<!-- 3. Press <kbd>Ctrl+Shift+B</kbd> to start the build process. -->
1. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `/.scripts/code.bat` (Windows). This should open up the built IDE!
4. In a new terminal, run `./scripts/code.sh` (Mac/Linux) or `./scripts/code.bat` (Windows). This should open up the built IDE!
You can always press <kbd>Ctrl+Shift+P</kbd> and run "Reload Window" inside the new window to see changes without re-building.
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page.
### Common Fixes
@@ -118,15 +91,8 @@ Here are the full options: `vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin
## Roadmap
Here are the most important topics on our Roadmap. More ⭐'s = more important. Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
Please refer to our [Issues](https://github.com/voideditor/void/issues) page for the latest issues.
## ⭐⭐⭐ 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.
## ⭐⭐⭐ 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.
## ⭐⭐⭐ Creative.
@@ -134,10 +100,6 @@ Examples: creating better code search, or supporting AI agents that can edit acr
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.
⭐ Let the user Accept / Reject all Diffs in an entire file via the sidebar.
# Guidelines
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project. Feel free to shoot us a message in the #general channel of the [Discord](https://discord.gg/RSNjgaugJs) for any reason. Please check in especially if you want to make a lot of changes or build a large new feature.
@@ -146,7 +108,7 @@ We're always glad to talk about new ideas, help you get set up, and make sure yo
## Submitting a Pull Request
Please submit a pull request once you've made a change. You don't need to submit an issue.
Please submit a pull request once you've made a change. You don't need to submit an issue.
Please don't use AI to write your PR 🙂.
+1 -1
View File
@@ -18,7 +18,7 @@ The Void team put together this list of links to get up and running with VSCode'
## 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"
]
}
-22
View File
@@ -1,22 +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}",
"--enable-proposed-api=void.void",
],
"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").
-64
View File
@@ -1,64 +0,0 @@
const tailwindcss = require('tailwindcss')
const autoprefixer = require('autoprefixer')
const postcss = require('postcss')
const fs = require('fs')
const convertTailwindToCSS = ({ from, to }) => {
console.log('converting ', from, ' --> ', to)
const original_css_contents = fs.readFileSync(from, 'utf8')
return 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)
})
}
const esbuild = require('esbuild')
const convertTSXtoJS = async ({ from, to }) => {
console.log('converting ', from, ' --> ', to)
return esbuild.build({
entryPoints: [from],
bundle: true,
minify: true,
sourcemap: true,
outfile: to,
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
platform: 'browser',
external: ['vscode'],
}).catch(() => process.exit(1));
}
(async () => {
// convert tsx to js
await convertTSXtoJS({
from: 'src/webviews/sidebar/index.tsx',
to: 'dist/webviews/sidebar/index.js',
})
await convertTSXtoJS({
from: 'src/webviews/ctrlk/index.tsx',
to: 'dist/webviews/ctrlk/index.js',
})
await convertTSXtoJS({
from: 'src/webviews/diffline/index.tsx',
to: 'dist/webviews/diffline/index.js',
})
// convert tailwind to css
await convertTailwindToCSS({
from: 'src/webviews/styles.css',
to: 'dist/webviews/styles.css',
})
})()
-9084
View File
File diff suppressed because it is too large Load Diff
-159
View File
@@ -1,159 +0,0 @@
{
"name": "void",
"publisher": "void",
"displayName": "Void",
"description": "",
"version": "0.0.1",
"engines": {
"vscode": "^1.92.0"
},
"categories": [
"Other"
],
"enabledApiProposals": [
"editorInsets"
],
"activationEvents": [],
"main": "./out/extension/extension.js",
"contributes": {
"configuration": {
"title": "Void",
"properties": {}
},
"commands": [
{
"command": "void.ctrl+l",
"title": "Show Sidebar"
},
{
"command": "void.ctrl+k",
"title": "Make Inline Edit"
},
{
"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.toggleSettings",
"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.toggleSettings",
"when": "view == 'void.viewnumberone'",
"group": "navigation"
}
]
}
},
"scripts": {
"vscode:prepublish": "npm run compile",
"compile": "tsc -p ./",
"watch": "tsc -watch -p ./",
"build": "rimraf dist && node build/build.js",
"pretest": "tsc -p ./ && eslint src --ext ts",
"test": "vscode-test"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.29.2",
"@eslint/js": "^9.9.1",
"@google/generative-ai": "^0.21.0",
"@monaco-editor/react": "^4.6.0",
"@rrweb/types": "^2.0.0-alpha.17",
"@types/diff": "^5.2.2",
"@types/diff-match-patch": "^1.0.36",
"@types/jest": "^29.5.12",
"@types/lodash": "^4.17.12",
"@types/mocha": "^10.0.8",
"@types/node": "^22.5.1",
"@types/react": "^18.3.4",
"@types/react-dom": "^18.3.0",
"@types/react-syntax-highlighter": "^15.5.13",
"@types/uuid": "^10.0.0",
"@types/vscode": "1.92.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",
"diff-match-patch": "^1.0.5",
"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",
"lodash": "^4.17.21",
"marked": "^14.1.0",
"ollama": "^0.5.9",
"openai": "^4.68.4",
"postcss": "^8.4.41",
"posthog-js": "^1.176.0",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-markdown": "^9.0.1",
"react-syntax-highlighter": "^15.6.1",
"rimraf": "^6.0.1",
"rrweb-snapshot": "^2.0.0-alpha.4",
"tailwindcss": "^3.4.10",
"typescript": "5.5.4",
"typescript-eslint": "^8.3.0",
"uuid": "^10.0.0"
}
}
@@ -1,365 +0,0 @@
import Anthropic from '@anthropic-ai/sdk';
import OpenAI from 'openai';
import { Ollama } from 'ollama/browser'
import { Content, GoogleGenerativeAI, GoogleGenerativeAIError, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { VoidConfig } from '../webviews/common/contextForConfig'
export type AbortRef = { current: (() => void) | null }
export type OnText = (newText: string, fullText: string) => void
export type OnFinalMessage = (input: string) => void
export type LLMMessageAnthropic = {
role: 'user' | 'assistant',
content: string,
}
export type LLMMessage = {
role: 'system' | 'user' | 'assistant',
content: string,
}
type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: OnFinalMessage,
onError: (error: string) => void,
voidConfig: VoidConfig,
abortRef: AbortRef,
}) => void
type SendLLMMessageFnTypeExternal = (params: {
messages: LLMMessage[],
onText: OnText,
onFinalMessage: (fullText: string) => void,
onError: (error: string) => void,
voidConfig: VoidConfig | null,
abortRef: AbortRef,
}) => void
const parseMaxTokensStr = (maxTokensStr: string) => {
// parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
let int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
if (Number.isNaN(int))
return undefined
return int
}
// Anthropic
const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig }) => {
const anthropic = new Anthropic({ apiKey: voidConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
// 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: voidConfig.anthropic.model,
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
});
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)
})
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('Invalid API key.')
}
else {
onError(error.message)
}
})
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
const abort = () => {
did_abort = true
stream.controller.abort() // TODO need to test this to make sure it works, it might throw an error
}
return { abort }
};
// Gemini
const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
abortRef.current = () => {
didAbort = true
}
const genAI = new GoogleGenerativeAI(voidConfig.gemini.apikey);
const model = genAI.getGenerativeModel({ model: voidConfig.gemini.model });
// remove system messages that get sent to Gemini
// str of all system messages
let 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 => {
abortRef.current = () => {
// response.stream.return(fullText)
didAbort = true;
}
for await (const chunk of response.stream) {
if (didAbort) return;
const newText = chunk.text();
fullText += newText;
onText(newText, fullText);
}
onFinalMessage(fullText);
})
.catch((error) => {
if (error instanceof GoogleGenerativeAIFetchError) {
if (error.status === 400) {
onError('Invalid API key.');
}
else {
onError(`${error.name}:\n${error.message}`);
}
}
else {
onError(error);
}
})
}
// OpenAI, OpenRouter, OpenAICompatible
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
abortRef.current = () => {
didAbort = true;
};
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
let maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
if (voidConfig.default.whichApi === 'openAI') {
openai = new OpenAI({ apiKey: voidConfig.openAI.apikey, dangerouslyAllowBrowser: true });
options = { model: voidConfig.openAI.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openRouter') {
openai = new OpenAI({
baseURL: "https://openrouter.ai/api/v1", apiKey: voidConfig.openRouter.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: voidConfig.openRouter.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openAICompatible') {
openai = new OpenAI({ baseURL: voidConfig.openAICompatible.endpoint, apiKey: voidConfig.openAICompatible.apikey, dangerouslyAllowBrowser: true })
options = { model: voidConfig.openAICompatible.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
}
openai.chat.completions
.create(options)
.then(async response => {
abortRef.current = () => {
// response.controller.abort()
didAbort = true;
}
// when receive text
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 - this catches errors of both .create() and .then(for await)
.catch(error => {
if (error instanceof OpenAI.APIError) {
if (error.status === 401) {
onError('Invalid API key.');
}
else {
onError(`${error.name}:\n${error.message}`);
}
}
else {
onError(error);
}
})
};
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ""
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
abortRef.current = () => {
didAbort = true;
};
const ollama = new Ollama({ host: voidConfig.ollama.endpoint })
ollama.chat({
model: voidConfig.ollama.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.maxTokens) } // this is max_tokens
})
.then(async stream => {
abortRef.current = () => {
// stream.abort()
didAbort = true
}
// iterate through the stream
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 => {
onError(error)
})
};
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
let didAbort = false
let fullText = ''
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
abortRef.current = () => {
didAbort = true
}
fetch('https://api.greptile.com/v2/query', {
method: 'POST',
headers: {
"Authorization": `Bearer ${voidConfig.greptile.apikey}`,
"X-Github-Token": `${voidConfig.greptile.githubPAT}`,
"Content-Type": `application/json`,
},
body: JSON.stringify({
messages,
stream: true,
repositories: [voidConfig.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 => {
onError(e)
});
}
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, onError, voidConfig, abortRef }) => {
if (!voidConfig) return;
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
switch (voidConfig.default.whichApi) {
case 'anthropic':
return sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'openAI':
case 'openRouter':
case 'openAICompatible':
return sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'gemini':
return sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'ollama':
return sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
case 'greptile':
return sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, abortRef });
default:
onError(`Error: whichApi was ${voidConfig.default.whichApi}, which is not recognized!`)
}
}
@@ -1,97 +0,0 @@
import * as vscode from 'vscode';
import { PartialVoidConfig } from '../webviews/common/contextForConfig'
type CodeSelection = { selectionStr: string, filePath: vscode.Uri }
type File = { filepath: vscode.Uri, content: string }
// an area that is currently being diffed
type DiffArea = {
diffareaid: number,
startLine: number,
endLine: number,
originalStartLine: number,
originalEndLine: number,
sweepIndex: number | null // null iff not sweeping
}
// the return type of diff creator
type BaseDiff = {
type: 'edit' | 'insertion' | 'deletion';
// repr: string; // representation of the diff in text
originalRange: vscode.Range;
originalCode: string;
range: vscode.Range;
code: string;
}
// each diff on the user's screen
type Diff = {
diffid: number,
lenses: vscode.CodeLens[],
} & BaseDiff
// editor -> sidebar
type MessageToSidebar = (
| { type: 'ctrl+l', selection: CodeSelection } // user presses ctrl+l in the editor. selection and path are frozen snapshots
| { type: 'ctrl+k', selection: CodeSelection }
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
| { type: 'partialVoidConfig', partialVoidConfig: PartialVoidConfig }
| { type: 'allThreads', threads: ChatThreads }
| { type: 'startNewThread' }
| { type: 'toggleThreadSelector' }
| { type: 'toggleSettings' }
| { type: 'deviceId', deviceId: string }
)
// sidebar -> editor
type MessageFromSidebar = (
| { type: 'applyChanges', diffRepr: string } // user clicks "apply" in the sidebar
| { type: 'requestFiles', filepaths: vscode.Uri[] }
| { type: 'getPartialVoidConfig' }
| { type: 'persistPartialVoidConfig', partialVoidConfig: PartialVoidConfig }
| { type: 'getAllThreads' }
| { type: 'persistThread', thread: ChatThreads[string] }
| { type: 'getDeviceId' }
)
type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string; // ISO string
lastModified: string; // ISO 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 | undefined; // content displayed to user (this is the same as content for now)
}
| {
role: "system";
content: string;
displayContent?: undefined;
}
export {
BaseDiff, Diff,
DiffArea,
CodeSelection,
File,
MessageFromSidebar,
MessageToSidebar,
ChatThreads,
ChatMessage,
}
@@ -1,577 +0,0 @@
import * as vscode from 'vscode';
import { findDiffs } from './findDiffs';
import { throttle } from 'lodash';
import { DiffArea, BaseDiff, Diff } from '../common/shared_types';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { updateWebviewHTML } from './extensionLib/updateWebviewHTML';
const THROTTLE_TIME = 100
// 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:...
})
const lightGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgba(218 218 218 / .2)',
isWholeLine: true,
})
const darkGrayDecoration = vscode.window.createTextEditorDecorationType({
backgroundColor: 'rgb(148 148 148 / .2)',
isWholeLine: true,
})
// responsible for displaying diffs and showing accept/reject buttons
export class DiffProvider implements vscode.CodeLensProvider {
private _originalFileOfDocument: { [docUriStr: string]: string } = {}
private _diffAreasOfDocument: { [docUriStr: string]: DiffArea[] } = {}
private _diffsOfDocument: { [docUriStr: string]: Diff[] } = {}
private _diffareaidPool = 0
private _diffidPool = 0
private _extensionUri: vscode.Uri
// 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(context: vscode.ExtensionContext) {
this._extensionUri = context.extensionUri
console.log('Creating DisplayChangesProvider')
// this acts as a useEffect every time text changes
vscode.workspace.onDidChangeTextDocument((e) => {
const editor = vscode.window.activeTextEditor
if (!editor) return
const docUriStr = editor.document.uri.toString()
const changes = e.contentChanges.map(c => ({ startLine: c.range.start.line, endLine: c.range.end.line, text: c.text, }))
// on user change, grow/shrink/merge/delete diff areas
this.refreshDiffAreasModel(docUriStr, changes, 'currentFile')
// refresh the diffAreas
this.refreshStylesAndDiffs(docUriStr)
})
}
// used by us only
public createDiffArea(uri: vscode.Uri, partialDiffArea: Omit<DiffArea, 'diffareaid'>, originalFile: string) {
const uriStr = uri.toString()
this._originalFileOfDocument[uriStr] = originalFile
// 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 > partialDiffArea.endLine || da.endLine < partialDiffArea.startLine
if (!noOverlap) return false
return true
})
// add `diffArea` to storage
const diffArea = {
...partialDiffArea,
diffareaid: this._diffareaidPool
}
this._diffAreasOfDocument[uriStr].push(diffArea)
this._diffareaidPool += 1
return diffArea
}
// used by us only
// changes the start/line locations based on the changes that were recently made. does not change any of the diffs in the diff areas
// changes tells us how many lines were inserted/deleted so we can grow/shrink the diffAreas accordingly
public refreshDiffAreasModel(docUriStr: string, changes: { text: string, startLine: number, endLine: number }[], changesTo: 'originalFile' | 'currentFile') {
const diffAreas = this._diffAreasOfDocument[docUriStr] || []
let endName
let startName
if (changesTo === 'originalFile') {
endName = 'originalEndLine' as const
startName = 'originalStartLine' as const
} else {
endName = 'endLine' as const
startName = 'startLine' as const
}
for (const change of changes) {
// 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.endLine - change.startLine
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.startLine >= diffArea[startName] && change.endLine <= diffArea[endName]) {
diffArea[endName] += deltaNewlines
}
// check if the `diffArea` was fully deleted and remove it if so
if (diffArea[startName] > diffArea[endName]) {
//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[startName] > change.endLine) {
diffArea[startName] += deltaNewlines
diffArea[endName] += deltaNewlines
}
}
// TODO merge any diffAreas if they overlap with each other as a result from the shift
}
}
// used by us only
// refreshes all the diffs inside each diff area, and refreshes the styles
public refreshStylesAndDiffs(docUriStr: string) {
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 originalFile = this._originalFileOfDocument[docUriStr]
if (!originalFile) {
console.log('Error: No original file!')
return;
}
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 originalCode = originalFile.split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
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(originalCode, currentCode)
// add the diffs to `this._diffsOfDocument[docUriStr]`
this.createDiffs(editor.document.uri, diffs, diffArea)
// // print diffs
// console.log('!ORIGINAL FILE:', JSON.stringify(originalFile))
// console.log('!NEW FILE :', JSON.stringify(editor.document.getText().replace(/\r\n/g, '\n')))
// console.log('!AREA originalCode:', JSON.stringify(originalCode))
// console.log('!AREA currentCode :', JSON.stringify(currentCode))
// for (const diff of this._diffsOfDocument[docUriStr]) {
// console.log('------------')
// console.log('originalCode:', JSON.stringify(diff.originalCode))
// console.log('currentCode:', JSON.stringify(diff.code))
// console.log('originalRange:', diff.originalRange.start.line, diff.originalRange.end.line,)
// console.log('currentRange:', diff.range.start.line, diff.range.end.line,)
// }
}
// update green highlighting
editor.setDecorations(
greenDecoration,
(this._diffsOfDocument[docUriStr]
.filter(diff => diff.range !== undefined)
.map(diff => diff.range)
)
);
// update red highlighting
// this._diffsOfDocument[docUriStr]
// .filter(diff => diff.originalCode !== '')
// .forEach(diff => {
// const text = originalFile.split('\n').slice(diff.originalRange.start.line, diff.originalRange.start.line + 1).join('\n')
// const height = text.split('\n').length
// const line = diff.range.start.line - 1
// const inset = vscode.window.createWebviewTextEditorInset(editor, line, height);
// updateWebviewHTML(inset.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/diffline/index.js', cssOutLocation: 'dist/webviews/styles.css' },
// { text }
// )
// })
// for each diffArea, highlight its sweepIndex in dark gray
editor.setDecorations(
darkGrayDecoration,
(this._diffAreasOfDocument[docUriStr]
.filter(diffArea => diffArea.sweepIndex !== null)
.map(diffArea => {
let s = diffArea.sweepIndex!
return new vscode.Range(s, 0, s, 0)
})
)
)
// for each diffArea, highlight sweepIndex+1...end in light gray
editor.setDecorations(
lightGrayDecoration,
(this._diffAreasOfDocument[docUriStr]
.filter(diffArea => diffArea.sweepIndex !== null)
.map(diffArea => {
return new vscode.Range(diffArea.sweepIndex! + 1, 0, diffArea.endLine, 0)
})
)
)
// update code lenses
this._onDidChangeCodeLenses.fire()
}
// used by us only
public createDiffs(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.range, { title: 'Accept', command: 'void.acceptDiff', arguments: [{ diffid: this._diffidPool, diffareaid: diffArea.diffareaid }] }),
new vscode.CodeLens(suggestedDiff.range, { 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
const docUriStr = editor.document.uri.toString()
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; }
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 originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri)
// Fixed: Handle newlines properly by splitting into lines and joining with proper newlines
const originalLines = originalFile.split('\n');
const currentLines = currentFile.split('\n');
// Get the changed lines from current file
const changedLines = currentLines.slice(diff.range.start.line, diff.range.end.line + 1);
// Create new original file content by replacing the affected lines
const newOriginalLines = [
...originalLines.slice(0, diff.originalRange.start.line),
...changedLines,
...originalLines.slice(diff.originalRange.end.line + 1)
];
this._originalFileOfDocument[docUriStr] = newOriginalLines.join('\n');
// Update diff areas based on the change
this.refreshDiffAreasModel(docUriStr, [{
text: changedLines.join('\n'),
startLine: diff.originalRange.start.line,
endLine: diff.originalRange.end.line
}], 'originalFile')
// Check if diffArea should be removed
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = newOriginalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
console.log('ACCEPT change', changedLines.join('\n'), diff.originalRange.start.line, diff.originalRange.end.line)
console.log('ACCEPT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
console.log('ACCEPT currentArea', currentArea)
console.log('ACCEPT originalArea', originalArea)
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
this.refreshStylesAndDiffs(docUriStr)
}
// called on void.rejectDiff
public async rejectDiff({ diffid, diffareaid }: { diffid: number, diffareaid: number }) {
const editor = vscode.window.activeTextEditor
if (!editor)
return
const docUriStr = editor.document.uri.toString()
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; }
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]
// Apply the rejection by replacing with original code
// we don't have to edit the original or final file; just do a workspace edit so the code equals the original code
const workspaceEdit = new vscode.WorkspaceEdit();
workspaceEdit.replace(editor.document.uri, diff.range, diff.originalCode)
await vscode.workspace.applyEdit(workspaceEdit)
// Check if diffArea should be removed
const originalFile = this._originalFileOfDocument[docUriStr]
const currentFile = await readFileContentOfUri(editor.document.uri)
const diffArea = this._diffAreasOfDocument[docUriStr][diffareaIdx]
const currentLines = currentFile.split('\n');
const originalLines = originalFile.split('\n');
const currentArea = currentLines.slice(diffArea.startLine, diffArea.endLine + 1).join('\n')
const originalArea = originalLines.slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
console.log('REJECT diff lines', diff.originalRange.start.line, diff.originalRange.end.line)
console.log('REJECT area lines', diffArea.startLine, diffArea.endLine, diffArea.originalStartLine, diffArea.originalEndLine)
console.log('REJECT currentArea', currentArea)
console.log('REJECT originalArea', originalArea)
if (originalArea === currentArea) {
const index = this._diffAreasOfDocument[docUriStr].findIndex(da => da.diffareaid === diffArea.diffareaid)
this._diffAreasOfDocument[docUriStr].splice(index, 1)
}
this.refreshStylesAndDiffs(docUriStr)
}
// used by us only
public updateStream = throttle(async (docUriStr: string, diffArea: DiffArea, newDiffAreaCode: string) => {
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;
}
// original code all diffs are based on in the code
const originalDiffAreaCode = (this._originalFileOfDocument[docUriStr] || '').split('\n').slice(diffArea.originalStartLine, diffArea.originalEndLine + 1).join('\n')
// figure out where to highlight based on where the AI is in the stream right now, use the last diff in findDiffs to figure that out
const diffs = findDiffs(originalDiffAreaCode, newDiffAreaCode)
const lastDiff = diffs?.[diffs.length - 1] ?? null
// these are two different coordinate systems - new and old line number
let newFileEndLine: number // get new[0...newStoppingPoint] with line=newStoppingPoint highlighted
let oldFileStartLine: number // get original[oldStartingPoint...]
if (!lastDiff) {
// if the writing is identical so far, display no changes
newFileEndLine = 0
oldFileStartLine = 0
}
else {
if (lastDiff.type === 'insertion') {
newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else if (lastDiff.type === 'deletion') {
newFileEndLine = lastDiff.range.start.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else if (lastDiff.type === 'edit') {
newFileEndLine = lastDiff.range.end.line
oldFileStartLine = lastDiff.originalRange.start.line
}
else {
throw new Error(`updateStream: diff.type not recognized: ${lastDiff.type}`)
}
}
// display
const newFileTop = newDiffAreaCode.split('\n').slice(0, newFileEndLine + 1).join('\n')
const oldFileBottom = originalDiffAreaCode.split('\n').slice(oldFileStartLine + 1, Infinity).join('\n')
let newCode = `${newFileTop}\n${oldFileBottom}`
diffArea.sweepIndex = newFileEndLine
// replace oldDACode with newDACode with a vscode edit
const workspaceEdit = new vscode.WorkspaceEdit();
const diffareaRange = new vscode.Range(diffArea.startLine, 0, diffArea.endLine, Number.MAX_SAFE_INTEGER)
workspaceEdit.replace(editor.document.uri, diffareaRange, newCode)
await vscode.workspace.applyEdit(workspaceEdit)
}, THROTTLE_TIME)
}
/*
import * as vscode from 'vscode';
import { SuggestedEdit } from './findDiffs';
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:...
})
export class DiffProvider {
originalCodeOfDocument: { [docUri: string]: string }
diffsOfDocument: {
[docUri: string]: {
startLine,
startCol,
endLine,
endCol,
originalText,
inset,
diffid,
}
}
// sweep
currentLine: { [docUri: string]: undefined | number }
weAreEditing: boolean = false
constructor() {
vscode.workspace.onDidChangeTextDocument((e) => {
// on user change, grow/shrink/merge/delete diff AREAS
// you dont have to do anything to the diffs here bc they all get recomputed in refresh()
// user changes only get highlighted if theyre in a diffarea
// go thru all diff areas and adjust line numbers based on the user's change
this.refreshStyles(e.document.uri.toString())
})
}
// refreshes styles on page
refreshStyles(docUriStr: string) {
if (this.weAreEditing) return
// recompute all diffs on the page
// run inset.dispose() on all diffs
// original and current code -> diffs
// originalCodeOfDocument[docUriStr]
// create new diffs
const inset = vscode.window.createWebviewTextEditorInset(editor, lineStart, height, {})
inset.webview.html = `
<html>
<body style="pointer-events:none;">Hello World!</body>
</html>
`;
}
// called on void.acceptDiff
public async acceptDiff({ diffid }: { diffid: number }) {
// update original based on the diff
// refresh()
}
// called on void.rejectDiff
public async rejectDiff({ diffid }: { diffid: number }) {
// get diffs[diffid]
// revert current file based on diff
// refresh()
}
// sweep
initializeSweep({ startLine }) {
// reject all diffs on the page
// store original code
// currentLine=start of sweep
}
onUpdateSweep(addedText) {
// update final
// refresh() ?
// currentLine += number of newlines in addedText
}
onAbortSweep() {
}
}
*/
@@ -1,181 +0,0 @@
import * as vscode from 'vscode';
import { AbortRef, sendLLMMessage } from '../common/sendLLMMessage';
import { DiffArea } from '../common/shared_types';
import { writeFileWithDiffInstructions, searchDiffChunkInstructions } from '../common/systemPrompts';
import { VoidConfig } from '../webviews/common/contextForConfig';
import { DiffProvider } from './DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
const LINES_PER_CHUNK = 20 // number of lines to search at a time
type CompetedReturn = { isFinished: true, } | { isFinished?: undefined, }
const streamChunk = ({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, diffArea, voidConfig, abortRef }: { diffProvider: DiffProvider, docUri: vscode.Uri, oldFileStr: string, completedStr: string, diffRepr: string, voidConfig: VoidConfig, diffArea: DiffArea, abortRef: AbortRef }) => {
const NUM_MATCHUP_TOKENS = 20
const promptContent = `ORIGINAL_FILE
\`\`\`
${oldFileStr}
\`\`\`
DIFF
\`\`\`
${diffRepr}
\`\`\`
INSTRUCTIONS
Please finish writing the new file \`NEW_FILE\`. Return ONLY the completion of the file, without any explanation.
NEW_FILE
\`\`\`
${completedStr}
\`\`\`
`
// create a promise that can be awaited
return new Promise<CompetedReturn>((resolve, reject) => {
let isAnyChangeSoFar = false
// make LLM complete the file to include the diff
sendLLMMessage({
messages: [{ role: 'system', content: writeFileWithDiffInstructions, }, { role: 'user', content: promptContent, }],
onText: (newText, fullText) => {
const fullCompletedStr = completedStr + fullText
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
// if there was any change from the original file
if (!oldFileStr.includes(fullCompletedStr)) {
isAnyChangeSoFar = true
}
const isRecentMatchup = false
// the final NUM_MATCHUP_TOKENS characters of fullCompletedStr are the same as the final NUM_MATCHUP_TOKENS characters of the last item in the diffs of oldFileStr that had 0 changes
if (isAnyChangeSoFar && isRecentMatchup) {
diffProvider.updateStream(docUri.toString(), diffArea, fullCompletedStr)
// TODO resolve the promise
// resolve({ speculativeIndex: newCurrentLine + 1 });
// abort the LLM call
abortRef.current?.()
}
},
onFinalMessage: (fullText) => {
const newCompletedStr = completedStr + fullText
diffProvider.updateStream(docUri.toString(), diffArea, newCompletedStr)
resolve({ isFinished: true });
},
onError: (e) => {
resolve({ isFinished: true });
console.error('Error rewriting file with diff', e);
},
voidConfig,
abortRef,
})
})
}
const shouldApplyDiff = ({ diffRepr, oldFileStr: fileStr, speculationStr, voidConfig, abortRef }: { diffRepr: string, oldFileStr: string, speculationStr: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
const promptContent = `DIFF
\`\`\`
${diffRepr}
\`\`\`
FILES
\`\`\`
${fileStr}
\`\`\`
SELECTION
\`\`\`
${speculationStr}
\`\`\`
Return \`true\` if ANY part of the chunk should be modified, and \`false\` if it should not be modified. You should respond only with \`true\` or \`false\` and nothing else.
`
// create new promise
return new Promise<boolean>((resolve, reject) => {
// send message to LLM
sendLLMMessage({
messages: [{ role: 'system', content: searchDiffChunkInstructions, }, { role: 'user', content: promptContent, }],
onFinalMessage: (finalMessage) => {
const containsTrue = finalMessage
.slice(-10) // check for `true` in last 10 characters
.toLowerCase()
.includes('true')
resolve(containsTrue)
},
onError: (e) => {
resolve(false);
console.error('Error in shouldApplyDiff: ', e)
},
onText: () => { },
voidConfig,
abortRef,
})
})
}
// lazily applies the diff to the file
// we chunk the text in the file, and ask an LLM whether it should edit each chunk
export const applyDiffLazily = async ({ docUri, oldFileStr, voidConfig, abortRef, diffRepr, diffProvider, diffArea }: { docUri: vscode.Uri, oldFileStr: string, diffRepr: string, voidConfig: VoidConfig, diffProvider: DiffProvider, diffArea: DiffArea, abortRef: AbortRef }) => {
// stateful variables
let speculativeIndex = 0
let writtenTextSoFar: string[] = []
while (speculativeIndex < oldFileStr.split('\n').length) {
const chunkStr = oldFileStr.split('\n').slice(speculativeIndex, speculativeIndex + LINES_PER_CHUNK).join('\n')
// ask LLM if we should apply the diff to the chunk
const START = new Date().getTime()
let shouldApplyDiff_ = await shouldApplyDiff({ oldFileStr, speculationStr: chunkStr, diffRepr, voidConfig, abortRef })
const END = new Date().getTime()
// if should not change the chunk
if (!shouldApplyDiff_) {
console.log('KEEP CHUNK time: ', END - START)
speculativeIndex += LINES_PER_CHUNK
writtenTextSoFar.push(chunkStr)
diffProvider.updateStream(docUri.toString(), diffArea, writtenTextSoFar.join('\n'))
continue;
}
// ask LLM to rewrite file with diff (if there is significant matchup with the original file, we stop rewriting)
const START2 = new Date().getTime()
const completedStr = (await readFileContentOfUri(docUri)).split('\n').slice(0, speculativeIndex).join('\n');
const result = await streamChunk({ diffProvider, docUri, oldFileStr, completedStr, diffRepr, voidConfig, diffArea, abortRef, })
const END2 = new Date().getTime()
console.log('EDIT CHUNK time: ', END2 - START2);
// if we are finished, stop the loop
if (result.isFinished) {
break;
}
// TODO
// speculativeIndex = result.speculativeIndex
}
}
-100
View File
@@ -1,100 +0,0 @@
import * as vscode from 'vscode';
import { AbortRef, OnFinalMessage, OnText, sendLLMMessage } from "../common/sendLLMMessage"
import { VoidConfig } from '../webviews/common/contextForConfig';
import { searchDiffChunkInstructions, writeFileWithDiffInstructions } from '../common/systemPrompts';
import { throttle } from 'lodash';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
type Res<T> = ((value: T) => void)
const THRTOTLE_TIME = 100 // minimum time between edits
const LINES_PER_CHUNK = 20 // number of lines to search at a time
const applyCtrlLChangesToFile = throttle(
({ fileUri, newCurrentLine, oldCurrentLine, fullCompletedStr, oldFileStr, debug }: { fileUri: vscode.Uri, newCurrentLine: number, oldCurrentLine: number, fullCompletedStr: string, oldFileStr: string, debug?: string }) => {
// write the change to the file
const WRITE_TO_FILE = (
fullCompletedStr.split('\n').slice(0, newCurrentLine + 1).join('\n') // newFile[:newCurrentLine+1]
+ oldFileStr.split('\n').slice(oldCurrentLine + 1).join('\n') // oldFile[oldCurrentLine+1:]
)
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), WRITE_TO_FILE)
vscode.workspace.applyEdit(workspaceEdit)
// highlight the `newCurrentLine` in white
// highlight the remaining part of the file in gray
},
THRTOTLE_TIME, { trailing: true }
)
const applyCtrlK = async ({ fileUri, startLine, endLine, instructions, voidConfig, abortRef }: { fileUri: vscode.Uri, startLine: number, endLine: number, instructions: string, voidConfig: VoidConfig, abortRef: AbortRef }) => {
const fileStr = await readFileContentOfUri(fileUri)
const fileLines = fileStr.split('\n')
const prefix = fileLines.slice(startLine).join('\n')
const suffix = fileLines.slice(endLine + 1).join('\n')
const selection = fileLines.slice(startLine, endLine + 1).join('\n')
const promptContent = `Here is the user's original selection:
\`\`\`
<MID>${selection}</MID>
\`\`\`
The user wants to apply the following instructions to the selection:
${instructions}
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:
\`\`\`
<PRE>${prefix}</PRE>
<SUF>${suffix}</SUF>
<MID>`;
// TODO initialize stream
// update stream
sendLLMMessage({
messages: [{ role: 'user', content: promptContent, }],
onText: async (tokenStr, completionStr) => {
// TODO update stream
// apply the changes
const newCode = `${prefix}\n${completionStr}\n${suffix}`
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newCode)
vscode.workspace.applyEdit(workspaceEdit)
},
onFinalMessage: (completionStr) => {
// TODO end stream
// apply the changes
const newCode = `${prefix}\n${completionStr}\n${suffix}`
const workspaceEdit = new vscode.WorkspaceEdit()
workspaceEdit.replace(fileUri, new vscode.Range(0, 0, Number.MAX_SAFE_INTEGER, 0), newCode)
vscode.workspace.applyEdit(workspaceEdit)
},
onError: (e) => {
console.error('Error rewriting file with diff', e);
},
voidConfig,
abortRef,
})
}
export { applyCtrlK }
-200
View File
@@ -1,200 +0,0 @@
import * as vscode from 'vscode';
import { v4 as uuidv4 } from 'uuid'
import { AbortRef } from '../common/sendLLMMessage';
import { MessageToSidebar, MessageFromSidebar, DiffArea, ChatThreads } from '../common/shared_types';
import { getVoidConfigFromPartial } from '../webviews/common/contextForConfig';
import { applyDiffLazily } from './applyDiffLazily';
import { DiffProvider } from './DiffProvider';
import { readFileContentOfUri } from './extensionLib/readFileContentOfUri';
import { SidebarWebviewProvider } from './providers/SidebarWebviewProvider';
import { CtrlKWebviewProvider } from './providers/CtrlKWebviewProvider';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
const roundRangeToLines = (selection: vscode.Selection) => {
let endLine = selection.end.character === 0 ? selection.end.line - 1 : selection.end.line // e.g. if the user triple clicks, it selects column=0, line=line -> column=0, line=line+1
return new vscode.Range(selection.start.line, 0, endLine, Number.MAX_SAFE_INTEGER)
}
const getSelection = (editor: vscode.TextEditor) => {
// get the range of the selection and the file the user is in
const selectionRange = roundRangeToLines(editor.selection);
const selectionStr = editor.document.getText(selectionRange).trim();
const filePath = editor.document.uri;
return { selectionStr, filePath }
}
export function activate(context: vscode.ExtensionContext) {
// 1. Mount the chat sidebar
const sidebarWebviewProvider = new SidebarWebviewProvider(context);
context.subscriptions.push(
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, sidebarWebviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
);
// 1.5
const ctrlKWebviewProvider = new CtrlKWebviewProvider(context)
// 2. 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
const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
// 2.5: ctrl+k
context.subscriptions.push(
vscode.commands.registerCommand('void.ctrl+k', () => {
console.log('CTRLK PRESSED')
const editor = vscode.window.activeTextEditor
if (!editor) return
const { selectionStr, filePath } = getSelection(editor)
// send message to the webview (Sidebar.tsx)
// ctrlKWebviewProvider.onPressCtrlK()
// sidebarWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, filePath } } satisfies MessageToSidebar));
})
);
// 3. Show an approve/reject codelens above each change
const diffProvider = new DiffProvider(context);
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', diffProvider));
// 4. Add approve/reject commands
context.subscriptions.push(vscode.commands.registerCommand('void.acceptDiff', async (params) => {
diffProvider.acceptDiff(params)
}));
context.subscriptions.push(vscode.commands.registerCommand('void.rejectDiff', async (params) => {
diffProvider.rejectDiff(params)
}));
// 5. Receive messages from sidebar
sidebarWebviewProvider.webview.then(
webview => {
// top navigation bar commands
context.subscriptions.push(vscode.commands.registerCommand('void.startNewThread', async () => {
webview.postMessage({ type: 'startNewThread' } satisfies MessageToSidebar)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleThreadSelector', async () => {
webview.postMessage({ type: 'toggleThreadSelector' } satisfies MessageToSidebar)
}))
context.subscriptions.push(vscode.commands.registerCommand('void.toggleSettings', async () => {
webview.postMessage({ type: 'toggleSettings' } satisfies MessageToSidebar)
}));
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
webview.onDidReceiveMessage(async (m: MessageFromSidebar) => {
const abortApplyRef: AbortRef = { current: null }
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 MessageToSidebar)
}
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 partialDiffArea: Omit<DiffArea, 'diffareaid'> = {
startLine: 0, // in ctrl+L the start and end lines are the full document
endLine: editor.document.lineCount,
originalStartLine: 0,
originalEndLine: editor.document.lineCount,
sweepIndex: null,
}
const diffArea = diffProvider.createDiffArea(editor.document.uri, partialDiffArea, await readFileContentOfUri(editor.document.uri))
const docUri = editor.document.uri
const fileStr = await readFileContentOfUri(docUri)
const voidConfig = getVoidConfigFromPartial(context.globalState.get('partialVoidConfig') ?? {})
await applyDiffLazily({ docUri, oldFileStr: fileStr, diffRepr: m.diffRepr, voidConfig, diffProvider, diffArea, abortRef: abortApplyRef })
}
else if (m.type === 'getPartialVoidConfig') {
const partialVoidConfig = context.globalState.get('partialVoidConfig') ?? {}
webview.postMessage({ type: 'partialVoidConfig', partialVoidConfig } satisfies MessageToSidebar)
}
else if (m.type === 'persistPartialVoidConfig') {
const partialVoidConfig = m.partialVoidConfig
context.globalState.update('partialVoidConfig', partialVoidConfig)
}
else if (m.type === 'getAllThreads') {
const threads: ChatThreads = context.workspaceState.get('allThreads') ?? {}
webview.postMessage({ type: 'allThreads', threads } satisfies MessageToSidebar)
}
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 if (m.type === 'getDeviceId') {
let deviceId = context.globalState.get('void_deviceid')
if (!deviceId || typeof deviceId !== 'string') {
deviceId = uuidv4()
context.globalState.update('void_deviceid', deviceId)
}
webview.postMessage({ type: 'deviceId', deviceId: deviceId as string } satisfies MessageToSidebar)
}
else {
console.error('unrecognized command', 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
// })
// )
}
@@ -1,14 +0,0 @@
import * as vscode from 'vscode'
export const readFileContentOfUri = async (uri: vscode.Uri): Promise<string> => {
const document = await vscode.workspace.openTextDocument(uri);
return document.getText().replace(/\r\n/g, '\n') ?? '' // Normalize line endings
};
// this is the old version, which only reads the most recently saved version
// export 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
// }
@@ -1,46 +0,0 @@
import * as vscode from 'vscode'
function generateNonce() {
let text = "";
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
for (let i = 0; i < 32; i++) {
text += possible.charAt(Math.floor(Math.random() * possible.length));
}
return text;
}
// call this when you have access to the webview to set its html
export const updateWebviewHTML = (webview: vscode.Webview, extensionUri: vscode.Uri, { jsOutLocation, cssOutLocation }: { jsOutLocation: string, cssOutLocation: string }, props?: object) => {
// 'dist/sidebar/index.js'
// 'dist/sidebar/styles.css'
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, jsOutLocation));
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri, cssOutLocation));
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(extensionUri));
const nonce = generateNonce();
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="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"${props ? ` data-void-props="${encodeURIComponent(JSON.stringify(props))}"` : ''}></div>
<script nonce="${nonce}" src="${scriptUri}"></script>
</body>
</html>`;
webview.html = webviewHTML
webview.options = {
enableScripts: true,
localResourceRoots: [extensionUri]
};
}
-132
View File
@@ -1,132 +0,0 @@
import { Range } from 'vscode';
import { diffLines, Change } from 'diff';
import { BaseDiff } from '../common/shared_types';
// class Range {
// range: any;
// constructor(startLine, startCol, endLine, endCol) {
// const range = {
// startLine,
// startCol,
// endLine,
// endCol,
// };
// this.range = range;
// }
// }
// Andrew diff algo:
export type SuggestedEdit = {
// start/end of current file
newRange: Range;
// start/end of original file
originalRange: Range;
type: 'insertion' | 'deletion' | 'edit',
originalContent: string, // original content (originalfile[originalStart...originalEnd])
newContent: string,
}
export function findDiffs(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);
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: BaseDiff[] = []
for (let 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'
let startLine = streakStartInNewFile
let endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
let startCol = 0
let endCol = Number.MAX_SAFE_INTEGER
let originalStartLine = streakStartInOldFile!
let originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
let originalStartCol = 0
let originalEndCol = Number.MAX_SAFE_INTEGER
let newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
let 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
startCol = 0
endCol = 0
newContent += '\n'
}
// INSERTION
else if (originalEndLine === originalStartLine - 1) {
type = 'insertion'
originalEndLine = originalStartLine
originalStartCol = 0
originalEndCol = 0
}
const replacement: BaseDiff = {
type,
range: new Range(startLine, startCol, endLine, endCol),
code: newContent,
originalRange: new Range(originalStartLine, originalStartCol, originalEndLine, originalEndCol),
originalCode: 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
}
@@ -1,57 +0,0 @@
// renders the code from `src/sidebar`
import * as vscode from 'vscode';
import { updateWebviewHTML as _updateWebviewHTML, updateWebviewHTML } from '../extensionLib/updateWebviewHTML';
// this comes from vscode.proposed.editorInsets.d.ts
declare module 'vscode' {
export interface WebviewEditorInset {
readonly editor: vscode.TextEditor;
readonly line: number;
readonly height: number;
readonly webview: vscode.Webview;
readonly onDidDispose: Event<void>;
dispose(): void;
}
export namespace window {
export function createWebviewTextEditorInset(editor: vscode.TextEditor, line: number, height: number, options?: vscode.WebviewOptions): WebviewEditorInset;
}
}
export class CtrlKWebviewProvider {
private readonly _extensionUri: vscode.Uri
private _idPool = 0
constructor(context: vscode.ExtensionContext) {
this._extensionUri = context.extensionUri
}
onPressCtrlK() {
// // TODO if currently selecting a ctrl k element, just focus it and do nothing
// const inset = vscode.window.createWebviewTextEditorInset(editor, line, height);
// const newCtrlKId = this._idPool++
// updateWebviewHTML(inset.webview, this._extensionUri, { jsOutLocation: 'dist/webviews/ctrlk/index.js', cssOutLocation: 'dist/webviews/styles.css' },
// { id: newCtrlKId }
// )
// ctrlKWebviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+k', selection: { selectionStr, selectionRange, filePath } } satisfies MessageToSidebar));
}
onDisposeCtrlK() {
}
}
@@ -1,30 +0,0 @@
// renders the code from `src/sidebar`
import * as vscode from 'vscode';
import { updateWebviewHTML as _updateWebviewHTML } from '../extensionLib/updateWebviewHTML';
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
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("Void sidebar provider: resolver was undefined")
this._res = temp_res
}
// called internally by vscode
resolveWebviewView(webviewView: vscode.WebviewView, context: vscode.WebviewViewResolveContext, token: vscode.CancellationToken,) {
const webview = webviewView.webview;
_updateWebviewHTML(webview, this._extensionUri, { jsOutLocation: 'dist/webviews/sidebar/index.js', cssOutLocation: 'dist/webviews/styles.css' })
this._res(webview); // resolve webview and _webviewView
}
}
@@ -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));
});
});
@@ -1,289 +0,0 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { awaitVSCodeResponse, getVSCodeAPI, useOnVSCodeMessage } from "./getVscodeApi"
const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
return {
description,
defaultVal,
enumArr,
}
}
const configString = (description: string, defaultVal: string) => {
return {
description,
defaultVal,
enumArr: undefined,
}
}
// fields you can customize (don't forget 'default' - it isn't included here!)
export const configFields = [
'anthropic',
'openAI',
'gemini',
'greptile',
'ollama',
'openRouter',
'openAICompatible',
'azure',
] as const
const voidConfigInfo: Record<
typeof configFields[number] | 'default', {
[prop: string]: {
description: string,
enumArr?: readonly string[] | undefined,
defaultVal: string,
},
}
> = {
default: {
whichApi: configEnum(
"API Provider.",
'anthropic',
configFields,
),
maxTokens: configEnum(
"Max number of tokens to output.",
'1024',
[
"default", // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
"1024",
"2048",
"4096",
"8192"
] as const,
),
},
anthropic: {
apikey: configString('Anthropic API key.', ''),
model: configEnum(
"Anthropic model to use.",
'claude-3-5-sonnet-20240620',
[
"claude-3-5-sonnet-20240620",
"claude-3-opus-20240229",
"claude-3-sonnet-20240229",
"claude-3-haiku-20240307"
] as const,
),
},
openAI: {
apikey: configString('OpenAI API key.', ''),
model: configEnum(
'OpenAI model to use.',
'gpt-4o',
[
"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"
] as const
),
},
greptile: {
apikey: configString('Greptile API key.', ''),
githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
remote: configEnum(
'Repo location',
'github',
[
'github',
'gitlab'
] as const
),
repository: configString('Repository identifier in "owner/repository" format.', ''),
branch: configString('Name of the branch to use.', 'main'),
},
ollama: {
endpoint: configString(
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode-webview://*" ollama serve`.',
'http://127.0.0.1:11434'
),
// TODO we should allow user to select model inside Void, but for now we'll just let them handle the Ollama setup on their own
// model: configEnum(
// 'Ollama model to use.',
// 'llama3.1',
// ["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"] as const
// ),
},
openRouter: {
model: configString(
'OpenRouter model to use.',
'openai/gpt-4o'
),
apikey: configString('OpenRouter API key.', ''),
},
openAICompatible: {
endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'),
model: configString('The name of the model to use.', 'gpt-4o'),
apikey: configString('Your API key.', ''),
},
azure: {
// "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."
// }
// }
// },
},
gemini: {
apikey: configString('Google API key.', ''),
model: configEnum(
'Gemini model to use.',
'gemini-1.5-flash',
[
"gemini-1.5-flash",
"gemini-1.5-pro",
"gemini-1.5-flash-8b",
"gemini-1.0-pro"
] as const
),
},
}
// this is the type that comes with metadata like desc, default val, etc
type VoidConfigInfo = typeof voidConfigInfo
export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number]
// this is the type that specifies the user's actual config
export type PartialVoidConfig = {
[K in keyof typeof voidConfigInfo]?: {
[P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal']
}
}
export type VoidConfig = {
[K in keyof typeof voidConfigInfo]: {
[P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal']
}
}
export const getVoidConfigFromPartial = (partialVoidConfig: PartialVoidConfig): VoidConfig => {
const config = {} as PartialVoidConfig
for (let field of [...configFields, 'default'] as const) {
config[field] = {}
for (let prop in voidConfigInfo[field]) {
config[field][prop] = partialVoidConfig[field]?.[prop]?.trim() || voidConfigInfo[field][prop].defaultVal
}
}
return config as VoidConfig
}
const defaultVoidConfig: VoidConfig = getVoidConfigFromPartial({})
// 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
}
type SetConfigParamType = <K extends VoidConfigField>(field: K, param: keyof VoidConfigInfo[K], newVal: string) => void
type ConfigValueType = {
voidConfig: VoidConfig,
voidConfigInfo: VoidConfigInfo,
partialVoidConfig: PartialVoidConfig,
setConfigParam: SetConfigParamType
}
const ConfigContext = createContext<ConfigValueType>(undefined as unknown as ConfigValueType)
export function ConfigProvider({ children }: { children: ReactNode }) {
const [partialVoidConfig, setPartialVoidConfig] = useInstantState<PartialVoidConfig>({}) // the user's selections
const [voidConfig, setVoidConfig] = useState<VoidConfig>(defaultVoidConfig)
// get the config on mount
useEffect(() => {
getVSCodeAPI().postMessage({ type: 'getPartialVoidConfig' })
awaitVSCodeResponse('partialVoidConfig').then((m) => {
setPartialVoidConfig(m.partialVoidConfig)
const newFullConfig = getVoidConfigFromPartial(m.partialVoidConfig)
setVoidConfig(newFullConfig)
})
}, [setPartialVoidConfig])
// return the provider
return (<ConfigContext.Provider
value={{
voidConfig,
voidConfigInfo,
partialVoidConfig: partialVoidConfig.current ?? {},
setConfigParam: (field, param, newVal) => {
const newPartialConfig: PartialVoidConfig = {
...partialVoidConfig.current,
[field]: {
...partialVoidConfig.current?.[field],
[param]: newVal
}
}
setPartialVoidConfig(newPartialConfig)
const newFullConfig = getVoidConfigFromPartial(newPartialConfig)
setVoidConfig(newFullConfig)
getVSCodeAPI().postMessage({ type: 'persistPartialVoidConfig', partialVoidConfig: newPartialConfig })
}
}}
>
{children}
</ConfigContext.Provider>
)
}
export function useVoidConfig(): ConfigValueType {
const context = useContext<ConfigValueType>(ConfigContext)
if (context === undefined) {
throw new Error("useVoidConfig missing Provider")
}
return context
}
@@ -1,36 +0,0 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
const PropsContext = createContext<any>(undefined as unknown as any)
// provider for whatever came in data-void-props
export function PropsProvider({ children, rootElement }: { children: ReactNode, rootElement: HTMLElement }) {
const [props, setProps] = useState<object | null>(null)
// update props when rootElement changes
useEffect(() => {
let props = rootElement.getAttribute("data-void-props")
let propsObj: object | null = null
if (props !== null) {
propsObj = JSON.parse(decodeURIComponent(props))
}
setProps(propsObj)
}, [rootElement])
return (
<PropsContext.Provider value={props}>
{children}
</PropsContext.Provider>
)
}
export function useVoidProps<T extends {}>(): T | null {
// context is the "value" from above
const context: T | null | undefined = useContext<T>(PropsContext)
// only undefined if has no provider
if (context === undefined) {
throw new Error("useVoidProps missing Provider")
}
return context
}
@@ -1,106 +0,0 @@
import React, { ReactNode, createContext, useCallback, useContext, useEffect, useRef, useState, } from "react"
import { ChatMessage, ChatThreads } from "../../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI } from "./getVscodeApi"
// a "thread" means a chat message history
type ConfigForThreadsValueType = {
readonly getAllThreads: () => ChatThreads;
readonly getCurrentThread: () => ChatThreads[string] | null;
addMessageToHistory: (message: ChatMessage) => void;
switchToThread: (threadId: string) => void;
startNewThread: () => void;
}
const ThreadsContext = createContext<ConfigForThreadsValueType>(undefined as unknown as ConfigForThreadsValueType)
const createNewThread = () => {
const now = new Date().toISOString()
return {
id: new Date().getTime().toString(),
createdAt: now,
lastModified: now,
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 [allThreadsRef, setAllThreads] = useInstantState<ChatThreads>({})
const [currentThreadIdRef, 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={{
getAllThreads: () => allThreadsRef.current ?? {},
getCurrentThread: () => currentThreadIdRef.current ? allThreadsRef.current?.[currentThreadIdRef.current] ?? null : null,
addMessageToHistory: (message: ChatMessage) => {
let currentThread: ChatThreads[string]
if (!(currentThreadIdRef.current === null || allThreadsRef.current === null)) {
currentThread = allThreadsRef.current[currentThreadIdRef.current]
}
else {
currentThread = createNewThread()
setCurrentThreadId(currentThread.id)
}
setAllThreads({
...allThreadsRef.current,
[currentThread.id]: {
...currentThread,
lastModified: new Date().toISOString(),
messages: [...currentThread.messages, message],
}
})
getVSCodeAPI().postMessage({ type: "persistThread", thread: currentThread })
},
switchToThread: (threadId: string) => {
setCurrentThreadId(threadId);
},
startNewThread: () => {
const newThread = createNewThread()
setAllThreads({
...allThreadsRef.current,
[newThread.id]: newThread
})
setCurrentThreadId(newThread.id)
},
}}
>
{children}
</ThreadsContext.Provider>
)
}
export function useThreads(): ConfigForThreadsValueType {
const context = useContext<ConfigForThreadsValueType>(ThreadsContext)
if (context === undefined) {
throw new Error("useThreads missing Provider")
}
return context
}
@@ -1,94 +0,0 @@
import { useEffect } from "react";
import { MessageFromSidebar, MessageToSidebar, } from "../../common/shared_types";
import { v4 as uuidv4 } from 'uuid';
type Command = MessageToSidebar['type']
// messageType -> res[]
const onetimeCallbacks: { [C in Command]: ((res: any) => void)[] } = {
"ctrl+l": [],
"ctrl+k": [],
"files": [],
"partialVoidConfig": [],
"startNewThread": [],
"allThreads": [],
"toggleThreadSelector": [],
"toggleSettings": [],
"deviceId": [],
}
// messageType -> id -> res
const callbacks: { [C in Command]: { [id: string]: ((res: any) => void) } } = {
"ctrl+l": {},
"ctrl+k": {},
"files": {},
"partialVoidConfig": {},
"startNewThread": {},
"allThreads": {},
"toggleThreadSelector": {},
"toggleSettings": {},
"deviceId": {}
}
// use this function to await responses
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
let result: Promise<MessageToSidebar & { type: C }> = new Promise((res, rej) => {
onetimeCallbacks[c].push(res)
})
return result
}
// use this function to add a listener to a certain type of message
export const useOnVSCodeMessage = <C extends Command>(messageType: C, fn: (e: MessageToSidebar & { type: C }) => void) => {
useEffect(() => {
const mType = messageType
const callbackId: string = uuidv4();
// @ts-ignore
callbacks[mType][callbackId] = fn;
return () => { delete callbacks[mType][callbackId] }
}, [messageType, fn])
}
// this function gets called whenever sidebar receives a message - it should only mount once
export const onMessageFromVSCode = (m: MessageToSidebar) => {
// resolve all promises for this message type
for (let res of onetimeCallbacks[m.type]) {
res(m)
onetimeCallbacks[m.type].splice(0) // clear the array
}
// call the listener for this message type
for (let res of Object.values(callbacks[m.type])) {
res(m)
}
}
type AcquireVsCodeApiType = () => {
postMessage(message: MessageFromSidebar): void;
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
// getState(): any;
};
// VS Code exposes the function acquireVsCodeApi() to us, this variable makes sure it only gets called once
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
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');
}
}
@@ -1,66 +0,0 @@
import React, { useEffect } from "react";
import * as ReactDOM from "react-dom/client"
import { MessageToSidebar } from "../../common/shared_types";
import { getVSCodeAPI, awaitVSCodeResponse, onMessageFromVSCode } from "./getVscodeApi";
import { initPosthog, identifyUser } from "./posthog";
import { ThreadsProvider } from "./contextForThreads";
import { ConfigProvider } from "./contextForConfig";
import { PropsProvider } from "./contextForProps";
const ListenersAndTracking = () => {
// initialize posthog
useEffect(() => {
initPosthog()
}, [])
// when we get the deviceid, identify the user
useEffect(() => {
getVSCodeAPI().postMessage({ type: 'getDeviceId' });
awaitVSCodeResponse('deviceId').then((m => {
identifyUser(m.deviceId)
}))
}, [])
// Receive messages from the VSCode extension
useEffect(() => {
const listener = (event: MessageEvent) => {
const m = event.data as MessageToSidebar;
onMessageFromVSCode(m)
}
window.addEventListener('message', listener);
return () => window.removeEventListener('message', listener)
}, [])
return null
}
export const mount = (children: React.ReactNode) => {
if (typeof document === "undefined") {
console.error("index.tsx error: document was undefined")
return
}
// mount the sidebar on the id="root" element
const rootElement = document.getElementById("root")!
// console.log("Void root Element:", rootElement)
const content = (<>
<ListenersAndTracking />
<PropsProvider rootElement={rootElement}>
<ThreadsProvider>
<ConfigProvider>
{children}
</ConfigProvider>
</ThreadsProvider>
</PropsProvider>
</>)
const root = ReactDOM.createRoot(rootElement)
root.render(content);
}
@@ -1,20 +0,0 @@
import posthog from 'posthog-js'
export const identifyUser = (id: string) => {
posthog.identify(id)
}
export const captureEvent = (eventId: string, properties: object) => {
posthog.capture(eventId, properties)
}
export const initPosthog = () => {
// We send absolutely no code to the server. We only track usage metrics like button clicks, etc. This might change and we might eventually add an opt-in or opt-out.
posthog.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2',
{
api_host: 'https://us.i.posthog.com',
person_profiles: 'identified_only' // we only track events from identified users. We identify them in Sidebar
}
)
}
@@ -1,27 +0,0 @@
import React, { useState } from 'react';
import { useOnVSCodeMessage } from '../common/getVscodeApi';
export const CtrlK = () => {
const [x, sx] = useState('abc')
useOnVSCodeMessage('ctrl+k', () => {
console.log('Ctrl+K pressed')
sx('Pressed ctrl+k')
})
// const inset = vscode.window.createWebviewTextEditorInset(editor, 10, 10, {})
// inset.webview.html = `
// <html>
// <body style="pointer-events:none;">Hello World!</body>
// </html>
// `;
return <>
<div>
{x}
</div>
</>
};
@@ -1,7 +0,0 @@
import React from "react"
import { mount } from "../common/mount"
import { CtrlK } from "./CtrlK"
// this is the entry point that mounts ctrlk
mount(<CtrlK />)
@@ -1,29 +0,0 @@
import React, { useState } from 'react';
import { useOnVSCodeMessage } from '../common/getVscodeApi';
import { useVoidProps } from '../common/contextForProps';
type props = {
text: string
}
export const DiffLine = () => {
const props = useVoidProps<props>()
console.log('props!', props)
if (!props) {
return null
}
// eslint-disable-next-line react/prop-types
const text = props.text
return <>
<div>
{text}
</div>
</>
};
@@ -1,7 +0,0 @@
import React from "react"
import { mount } from "../common/mount"
import { DiffLine } from "./DiffLine"
// this is the entry point that mounts diffline
mount(<DiffLine />)
@@ -1,67 +0,0 @@
import React, { useState, useEffect, useRef, useCallback, FormEvent } from "react"
import { CodeSelection, ChatMessage, MessageToSidebar } from "../../common/shared_types"
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi"
import { SidebarThreadSelector } from "./SidebarThreadSelector";
import { SidebarChat } from "./SidebarChat";
import { SidebarSettings } from "./SidebarSettings";
import { identifyUser } from "../common/posthog";
const Sidebar = () => {
const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
const [tab, setTab] = useState<'threadSelector' | 'chat' | 'settings'>('chat')
// if they pressed the + to add a new chat
useOnVSCodeMessage('startNewThread', (m) => {
setTab('chat');
chatInputRef.current?.focus();
})
// ctrl+l should switch back to chat
useOnVSCodeMessage('ctrl+l', (m) => {
setTab('chat');
chatInputRef.current?.focus();
})
// if they toggled thread selector
useOnVSCodeMessage('toggleThreadSelector', (m) => {
if (tab === 'threadSelector') {
setTab('chat')
chatInputRef.current?.blur();
} else
setTab('threadSelector')
})
// if they toggled settings
useOnVSCodeMessage('toggleSettings', (m) => {
if (tab === 'settings') {
setTab('chat')
chatInputRef.current?.blur();
} else
setTab('settings')
})
return <>
<div className={`flex flex-col h-screen w-full`}>
<div className={`mb-2 h-[30vh] ${tab !== 'threadSelector' ? 'hidden' : ''}`}>
<SidebarThreadSelector onClose={() => setTab('chat')} />
</div>
<div className={`${tab !== 'chat' && tab !== 'threadSelector' ? 'hidden' : ''}`}>
<SidebarChat chatInputRef={chatInputRef} />
</div>
<div className={`${tab !== 'settings' ? 'hidden' : ''}`}>
<SidebarSettings />
</div>
</div>
</>
}
export default Sidebar
@@ -1,382 +0,0 @@
import React, { FormEvent, useCallback, useEffect, useRef, useState } from "react";
import { marked } from 'marked';
import MarkdownRender from "./markdown/MarkdownRender";
import BlockCode from "./markdown/BlockCode";
import { File, ChatMessage, CodeSelection } from "../../common/shared_types";
import * as vscode from 'vscode'
import { awaitVSCodeResponse, getVSCodeAPI, onMessageFromVSCode, useOnVSCodeMessage } from "../common/getVscodeApi";
import { useThreads } from "../common/contextForThreads";
import { sendLLMMessage } from "../../common/sendLLMMessage";
import { useVoidConfig } from "../common/contextForConfig";
import { captureEvent } from "../common/posthog";
import { generateDiffInstructions } from "../../common/systemPrompts";
const filesStr = (fullFiles: File[]) => {
return fullFiles.map(({ filepath, content }) =>
`
${filepath.fsPath}
\`\`\`
${content}
\`\`\``).join('\n')
}
const userInstructionsStr = (instructions: string, files: File[], selection: CodeSelection | null) => {
let str = '';
if (files.length > 0) {
str += filesStr(files);
}
if (selection) {
str += `
I am currently selecting this code:
\t\`\`\`${selection.selectionStr}\`\`\`
`;
}
if (files.length > 0 && selection) {
str += `
Please edit the selected code or the entire file following these instructions:
`;
} else if (files.length > 0) {
str += `
Please edit the file following these instructions:
`;
} else if (selection) {
str += `
Please edit the selected code following these instructions:
`;
}
str += `
\t${instructions}
`;
if (files.length > 0) {
str += `
\tIf you make a change, rewrite the entire file.
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
}
return str;
};
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>
)
)
}
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}
buttonsOnHover={null}
/>}
{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>
}
export const SidebarChat = ({ chatInputRef }: { chatInputRef: React.RefObject<HTMLTextAreaElement> }) => {
// 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 abortFnRef = useRef<(() => void) | null>(null)
const [latestError, setLatestError] = useState('')
// higher level state
const { getAllThreads, getCurrentThread, addMessageToHistory, startNewThread, switchToThread } = useThreads()
const { voidConfig } = useVoidConfig()
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureChatEvent = useCallback((eventId: string, extras?: object) => {
const whichApi = voidConfig.default['whichApi']
const messages = getCurrentThread()?.messages
captureEvent(eventId, {
whichApi: whichApi,
numMessages: messages?.length,
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.displayContent?.length })),
version: '2024-10-19',
...extras,
})
}, [getCurrentThread, voidConfig.default])
// if they pressed the + to add a new chat
useOnVSCodeMessage('startNewThread', (m) => {
const allThreads = getAllThreads()
// find a thread with 0 messages and switch to it
for (let threadId in allThreads) {
if (allThreads[threadId].messages.length === 0) {
switchToThread(threadId)
return
}
}
// start a new thread
startNewThread()
})
// if user pressed ctrl+l, add their selection to the sidebar
useOnVSCodeMessage('ctrl+l', (m) => {
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])
})
const isDisabled = !instructions
const formRef = useRef<HTMLFormElement | null>(null)
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isDisabled) return
if (isLoading) return
setIsLoading(true)
setInstructions('');
formRef.current?.reset(); // reset the form's text when clear instructions or unexpected behavior happens
setSelection(null)
setFiles([])
setLatestError('')
// request file content from vscode and await response
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
const relevantFiles = await awaitVSCodeResponse('files')
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
addMessageToHistory(systemPromptElt)
const userContent = userInstructionsStr(instructions, relevantFiles.files, selection)
const newHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selection, files }
addMessageToHistory(newHistoryElt)
captureChatEvent('Chat - Sending Message', { messageLength: instructions.length })
const submit_time = new Date()
// send message to LLM
sendLLMMessage({
messages: [...(getCurrentThread()?.messages ?? []).map(m => ({ role: m.role, content: m.content })),],
onText: (newText, fullText) => setMessageStream(fullText),
onFinalMessage: (content) => {
captureChatEvent('Chat - Received Full Message', { messageLength: content.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
// add assistant's message to chat history, and clear selection
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
},
onError: (error) => {
captureChatEvent('Chat - Error', { error })
// add assistant's message to chat history, and clear selection
let content = messageStream; // just use the current content
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
setLatestError(error)
},
voidConfig,
abortRef: abortFnRef,
})
}
const onAbort = useCallback(() => {
captureChatEvent('Chat - Abort', { messageLengthSoFar: messageStream.length })
// abort claude
abortFnRef.current?.()
// if messageStream was not empty, add it to the history
const llmContent = messageStream || '(null)'
const newHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream, }
addMessageToHistory(newHistoryElt)
setMessageStream('')
setIsLoading(false)
}, [captureChatEvent, messageStream, addMessageToHistory])
return <>
<div className="overflow-x-hidden space-y-4">
{/* previous messages */}
{getCurrentThread() !== null && getCurrentThread()?.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 text={selection.selectionStr}
buttonsOnHover={(
<button
onClick={() => setSelection(null)}
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!')
onSubmit(e)
}}>
{/* input */}
<textarea
ref={chatInputRef}
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
/>
{isLoading ?
// stop button
<button
onClick={onAbort}
type='button'
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
>
<svg
className='scale-50'
stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M24 24H0V0h24v24z"></path>
</svg>
</button>
:
// submit button (up arrow)
<button
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={isDisabled}
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>
{/* error message */}
{!latestError ? null : <div>
{latestError}
</div>}
</div>
</>
}
@@ -1,105 +0,0 @@
import React, { useState } from "react";
import { configFields, useVoidConfig, VoidConfigField } from "../common/contextForConfig";
const SettingOfFieldAndParam = ({ field, param }: { field: VoidConfigField, param: string }) => {
const { voidConfig, partialVoidConfig, voidConfigInfo, setConfigParam } = useVoidConfig()
const { enumArr, defaultVal, description } = voidConfigInfo[field][param]
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
const updateState = (newValue: string) => { setConfigParam(field, param, newValue) }
const resetButton = <button
disabled={val === defaultVal}
title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
onClick={() => updateState(defaultVal)}
>
<svg
className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
fill="currentColor" strokeWidth="0" viewBox="0 0 16 16" height="200px" width="200px" xmlns="http://www.w3.org/2000/svg"><path fillRule="evenodd" clipRule="evenodd" d="M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z"></path>
</svg>
</button>
const inputElement = enumArr === undefined ?
// string
(<input
className='input p-1 w-full'
type="text"
value={val}
onChange={(e) => updateState(e.target.value)}
/>)
:
// enum
(<select
className='dropdown p-1 w-full'
value={val}
onChange={(e) => updateState(e.target.value)}
>
{enumArr.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>)
return <div>
<label className='hidden'>{param}</label>
<span>{description}</span>
<div className='flex items-center'>
{inputElement}
{resetButton}
</div>
</div>
}
export const SidebarSettings = () => {
const { voidConfig, voidConfigInfo } = useVoidConfig()
const current_field = voidConfig.default['whichApi'] as VoidConfigField
return (
<div className='space-y-4 py-2 overflow-y-auto'>
{/* choose the field */}
<div className='outline-vscode-input-bg'>
<SettingOfFieldAndParam
field='default'
param='whichApi'
/>
<SettingOfFieldAndParam
field='default'
param='maxTokens'
/>
</div>
<hr />
{/* render all fields, but hide the ones not visible for fast tab switching */}
{configFields.map(field => {
return <div
key={field}
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
>
{Object.keys(voidConfigInfo[field]).map((param) => (
<SettingOfFieldAndParam
key={param}
field={field}
param={param}
/>
))}
</div>
})}
{/* Remove this after 10/21/24, this is just to give developers a heads up about the recent change */}
<div className='pt-20'>
{`We recently updated Settings. To copy your old Void settings over, press Ctrl+Shift+P, `}
{`type 'Open User Settings (JSON)',`}
{` and look for 'void.'. `}
</div>
</div>
)
}
@@ -1,79 +0,0 @@
import React from "react";
import { ThreadsProvider, useThreads } from "../common/contextForThreads";
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
export const SidebarThreadSelector = ({ onClose }: { onClose: () => void }) => {
const { getAllThreads, getCurrentThread, switchToThread } = useThreads()
const allThreads = getAllThreads()
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? 1 : -1)
return (
<div className="flex flex-col gap-y-1">
{/* X button at top right */}
<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>
{/* a list of all the past threads */}
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>
const pastThread = allThreads[threadId]
let btnStringArr = []
let msg1 = truncate(allThreads[threadId].messages[0]?.displayContent ?? '(empty)')
btnStringArr.push(msg1)
let msg2 = truncate(allThreads[threadId].messages[1]?.displayContent ?? '')
if (msg2)
btnStringArr.push(msg2)
btnStringArr.push(allThreads[threadId].messages.length)
const btnString = btnStringArr.join(' / ')
return (
<button
key={pastThread.id}
className={`btn btn-sm rounded-sm ${pastThread.id === getCurrentThread()?.id ? "btn-primary" : "btn-secondary"}`}
onClick={() => switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
{btnString}
</button>
)
})}
</div>
</div>
)
}
@@ -1,7 +0,0 @@
import React from "react"
import Sidebar from "./Sidebar"
import { mount } from "../common/mount"
// this is the entry point that mounts the sidebar
mount(<Sidebar />)
-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. */
}
}
+3060 -442
View File
File diff suppressed because it is too large Load Diff
+23 -1
View File
@@ -75,6 +75,8 @@
"@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 +87,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 +99,19 @@
"@xterm/addon-webgl": "^0.19.0-beta.64",
"@xterm/headless": "^5.6.0-beta.64",
"@xterm/xterm": "^5.6.0-beta.64",
"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",
"open": "^8.4.2",
"posthog-node": "^4.3.1",
"tas-client-umd": "0.2.0",
"v8-inspect-profiler": "^0.1.1",
"vscode-oniguruma": "1.7.0",
@@ -115,16 +121,22 @@
"yazl": "^2.4.3"
},
"devDependencies": {
"@anthropic-ai/sdk": "^0.32.1",
"@google/generative-ai": "^0.21.0",
"@playwright/test": "^1.46.1",
"@swc/core": "1.3.62",
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
"@types/diff": "^6.0.0",
"@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",
@@ -156,6 +168,7 @@
"cssnano": "^6.0.3",
"debounce": "^1.0.0",
"deemon": "^1.8.0",
"diff": "^7.0.0",
"electron": "30.5.1",
"eslint": "8.36.0",
"eslint-plugin-header": "3.1.1",
@@ -190,6 +203,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",
@@ -198,6 +212,8 @@
"mocha-junit-reporter": "^2.2.1",
"mocha-multi-reporters": "^1.5.1",
"npm-run-all": "^4.1.5",
"ollama": "^0.5.9",
"openai": "^4.71.1",
"opn": "^6.0.0",
"original-fs": "^1.2.0",
"os-browserify": "^0.3.0",
@@ -207,16 +223,22 @@
"postcss-nesting": "^12.0.2",
"pump": "^1.0.1",
"rcedit": "^1.1.0",
"rimraf": "^2.2.8",
"react": "^18.3.1",
"react-dom": "^18.3.1",
"react-syntax-highlighter": "^15.6.1",
"rimraf": "^2.7.1",
"scope-tailwind": "^1.0.1",
"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",
+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",
]
}
+25 -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,13 @@ export class CodeApplication extends Disposable {
mainProcessElectronServer.registerChannel('logger', loggerChannel);
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
// Void
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-sendLLMMessage', sendLLMMessageChannel);
// 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,130 @@
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';
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;
}
}
@@ -0,0 +1,105 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, LLMMessageServiceParams, ProxyLLMMessageParams, ProxyLLMMessageAbortParams } from '../common/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 { IDisposable } from '../../../base/common/lifecycle.js';
// BROWSER IMPLEMENTATION OF SENDLLMMESSAGE
export const ISendLLMMessageService = createDecorator<ISendLLMMessageService>('sendLLMMessageService');
// defines an interface that node/ creates and browser/ uses
export interface ISendLLMMessageService {
readonly _serviceBrand: undefined;
sendLLMMessage: (params: LLMMessageServiceParams) => string;
abort: (requestId: string) => void;
}
export class SendLLMMessageService implements ISendLLMMessageService {
readonly _serviceBrand: undefined;
private readonly channel: IChannel;
private readonly _disposablesOfRequestId: Record<string, IDisposable[]> = {}
constructor(
@IMainProcessService mainProcessService: IMainProcessService // used as a renderer (only usable on client side)
) {
this.channel = mainProcessService.getChannel('void-channel-sendLLMMessage')
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service, not needed here
}
_addDisposable(requestId: string, disposable: IDisposable) {
if (!this._disposablesOfRequestId[requestId]) {
this._disposablesOfRequestId[requestId] = []
}
this._disposablesOfRequestId[requestId].push(disposable)
}
sendLLMMessage(params: LLMMessageServiceParams) {
const requestId_ = generateUuid();
const { onText, onFinalMessage, onError, ...proxyParams } = params;
// listen for listenerName='onText' | 'onFinalMessage' | 'onError', and call the original function on it
const onTextEvent: Event<ProxyOnTextPayload> = this.channel.listen('onText')
this._addDisposable(requestId_,
onTextEvent(e => {
if (requestId_ !== e.requestId) return;
onText(e)
})
)
const onFinalMessageEvent: Event<ProxyOnFinalMessagePayload> = this.channel.listen('onFinalMessage')
this._addDisposable(requestId_,
onFinalMessageEvent(e => {
if (requestId_ !== e.requestId) return;
onFinalMessage(e)
this._dispose(requestId_)
})
)
const onErrorEvent: Event<ProxyOnErrorPayload> = this.channel.listen('onError')
this._addDisposable(requestId_,
onErrorEvent(e => {
if (requestId_ !== e.requestId) return;
console.log('event onError', JSON.stringify(e))
onError(e)
this._dispose(requestId_)
})
)
// params will be stripped of all its functions
this.channel.call('sendLLMMessage', { ...proxyParams, requestId: requestId_ } satisfies ProxyLLMMessageParams);
return requestId_
}
private _dispose(requestId: string) {
if (!(requestId in this._disposablesOfRequestId)) return
for (const disposable of this._disposablesOfRequestId[requestId]) {
disposable.dispose()
}
delete this._disposablesOfRequestId[requestId]
}
abort(requestId: string) {
this.channel.call('abort', { requestId } satisfies ProxyLLMMessageAbortParams);
this._dispose(requestId)
}
}
registerSingleton(ISendLLMMessageService, SendLLMMessageService, InstantiationType.Delayed);
@@ -0,0 +1,32 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
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 { IMetricsService } from '../common/metricsService.js';
// BROWSER IMPLEMENTATION, calls channel
export class MetricsService implements IMetricsService {
readonly _serviceBrand: undefined;
private readonly channel: IChannel;
constructor(
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
) {
this.channel = mainProcessService.getChannel('void-channel-metrics')
}
// call capture on the channel
capture(...params: Parameters<IMetricsService['capture']>) {
this.channel.call('capture', params);
}
}
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
+210
View File
@@ -0,0 +1,210 @@
const configEnum = <EnumArr extends readonly string[]>(description: string, defaultVal: EnumArr[number], enumArr: EnumArr) => {
return {
description,
defaultVal,
enumArr,
}
}
const configString = (description: string, defaultVal: string) => {
return {
description,
defaultVal,
enumArr: undefined,
}
}
// fields you can customize (don't forget 'default' - it isn't included here!)
export const nonDefaultConfigFields = [
'anthropic',
'openAI',
'gemini',
'greptile',
'groq',
'ollama',
'openRouter',
'openAICompatible',
'azure',
] as const
export const voidConfigInfo: Record<
typeof nonDefaultConfigFields[number] | 'default', {
[prop: string]: {
description: string;
enumArr?: readonly string[] | undefined;
defaultVal: string;
};
}
> = {
default: {
whichApi: configEnum(
'API Provider.',
'anthropic',
nonDefaultConfigFields,
),
maxTokens: configEnum(
'Max number of tokens to output.',
'1024',
[
'default', // this will be parseInt'd into NaN and ignored by the API. Anything that's not a number has this behavior.
'1024',
'2048',
'4096',
'8192'
] as const,
),
},
anthropic: {
apikey: configString('Anthropic API key.', ''),
model: configEnum(
'Anthropic model to use.',
'claude-3-5-sonnet-20240620',
[
'claude-3-5-sonnet-20240620',
'claude-3-opus-20240229',
'claude-3-sonnet-20240229',
'claude-3-haiku-20240307'
] as const,
),
},
openAI: {
apikey: configString('OpenAI API key.', ''),
model: configEnum(
'OpenAI model to use.',
'gpt-4o',
[
'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'
] as const
),
},
greptile: {
apikey: configString('Greptile API key.', ''),
githubPAT: configString('Github PAT that Greptile uses to access your repository', ''),
remote: configEnum(
'Repo location',
'github',
[
'github',
'gitlab'
] as const
),
repository: configString('Repository identifier in "owner / repository" format.', ''),
branch: configString('Name of the branch to use.', 'main'),
},
groq: {
apikey: configString('Groq API key.', ''),
model: configEnum(
'Groq model to use.',
'mixtral-8x7b-32768',
[
"mixtral-8x7b-32768",
"llama2-70b-4096",
"gemma-7b-it"
] as const
),
},
ollama: {
endpoint: configString(
'The endpoint of your Ollama instance. Start Ollama by running `OLLAMA_ORIGINS="vscode - webview://*" ollama serve`.',
'http://127.0.0.1:11434'
),
model: configEnum(
'Ollama model to use.',
'codestral',
['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'] as const
),
},
openRouter: {
model: configString(
'OpenRouter model to use.',
'openai/gpt-4o'
),
apikey: configString('OpenRouter API key.', ''),
},
openAICompatible: {
endpoint: configString('The baseUrl (exluding /chat/completions).', 'http://127.0.0.1:11434/v1'),
model: configString('The name of the model to use.', 'gpt-4o'),
apikey: configString('Your API key.', ''),
},
azure: {
// '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.'
// }
// }
// },
},
gemini: {
apikey: configString('Google API key.', ''),
model: configEnum(
'Gemini model to use.',
'gemini-1.5-flash',
[
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-1.0-pro'
] as const
),
},
}
// this is the type that comes with metadata like desc, default val, etc
export type VoidConfigInfo = typeof voidConfigInfo
export type VoidConfigField = keyof typeof voidConfigInfo // typeof configFields[number]
// this is the type that specifies the user's actual config
export type PartialVoidConfig = {
[K in keyof typeof voidConfigInfo]?: {
[P in keyof typeof voidConfigInfo[K]]?: typeof voidConfigInfo[K][P]['defaultVal']
}
}
export type VoidConfig = {
[K in keyof typeof voidConfigInfo]: {
[P in keyof typeof voidConfigInfo[K]]: typeof voidConfigInfo[K][P]['defaultVal']
}
}
@@ -0,0 +1,59 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { VoidConfig } from './configTypes.js'
// ---------- type definitions ----------
export type OnText = (p: { newText: string, fullText: string }) => void
export type OnFinalMessage = (p: { fullText: string }) => void
export type OnError = (p: { error: Error | string }) => void
export type AbortRef = { current: (() => void) | null }
export type LLMMessage = {
role: 'system' | 'user' | 'assistant';
content: string;
}
export type LLMMessageServiceParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
messages: LLMMessage[];
voidConfig: VoidConfig | null;
logging: {
loggingName: string,
};
}
export type SendLLMMMessageParams = {
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
messages: LLMMessage[];
voidConfig: VoidConfig | null;
logging: {
loggingName: string,
};
abortRef: AbortRef;
}
// can't send functions across a proxy, use listeners instead
export const listenerNames = ['onText', 'onFinalMessage', 'onError'] as const
export type ProxyLLMMessageParams = Omit<LLMMessageServiceParams, typeof listenerNames[number]> & { requestId: string }
export type ProxyOnTextPayload = Parameters<OnText>[0] & { requestId: string }
export type ProxyOnFinalMessagePayload = Parameters<OnFinalMessage>[0] & { requestId: string }
export type ProxyOnErrorPayload = Parameters<OnError>[0] & { requestId: string }
export type ProxyLLMMessageAbortParams = { requestId: string }
@@ -0,0 +1,10 @@
import { createDecorator } from '../../instantiation/common/instantiation.js';
export interface IMetricsService {
readonly _serviceBrand: undefined;
capture(event: string, params: Record<string, any>): void;
}
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
@@ -0,0 +1,57 @@
import Anthropic from '@anthropic-ai/sdk';
import { parseMaxTokensStr, SendLLMMessageFnTypeInternal } from './util.js';
// Anthropic
type LLMMessageAnthropic = {
role: 'user' | 'assistant';
content: string;
}
export const sendAnthropicMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const thisConfig = voidConfig.anthropic
const anthropic = new Anthropic({ apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
// 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: thisConfig.model,
max_tokens: parseMaxTokensStr(voidConfig.default.maxTokens)!, // this might be undefined, but it will just throw an error for the user
});
// 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({ error: 'Invalid API key.' })
}
else {
onError({ error })
}
})
// TODO need to test this to make sure it works, it might throw an error
_setAborter(() => stream.controller.abort())
};
@@ -0,0 +1,48 @@
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
import { SendLLMMessageFnTypeInternal } from './util';
// Gemini
export const sendGeminiMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
const thisConfig = voidConfig.gemini
const genAI = new GoogleGenerativeAI(thisConfig.apikey);
const model = genAI.getGenerativeModel({ model: thisConfig.model });
// 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({ error: 'Invalid API key.' });
}
else {
onError({ error });
}
})
}
@@ -0,0 +1,64 @@
// Greptile
// https://docs.greptile.com/api-reference/query
// https://docs.greptile.com/quickstart#sample-response-streamed
import { SendLLMMessageFnTypeInternal } from './util';
export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
const thisConfig = voidConfig.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,40 @@
import Groq from 'groq-sdk';
import { SendLLMMessageFnTypeInternal } from './util';
import { parseMaxTokensStr } from './util.js';
// Groq
export const sendGroqMsg: SendLLMMessageFnTypeInternal = async ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = '';
const groq = new Groq({
apiKey: voidConfig.groq.apikey,
dangerouslyAllowBrowser: true
});
await groq.chat.completions
.create({
messages: messages,
model: voidConfig.groq.model,
stream: true,
temperature: 0.7,
max_tokens: parseMaxTokensStr(voidConfig.default.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({ error });
})
};
@@ -0,0 +1,47 @@
import { Ollama, ErrorResponse } from 'ollama';
import { SendLLMMessageFnTypeInternal } from './util';
import { parseMaxTokensStr } from './util.js';
// Ollama
export const sendOllamaMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
const thisConfig = voidConfig.ollama
let fullText = ''
const ollama = new Ollama({ host: thisConfig.endpoint })
ollama.chat({
model: thisConfig.model,
messages: messages,
stream: true,
options: { num_predict: parseMaxTokensStr(voidConfig.default.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({ error })
})
};
@@ -0,0 +1,64 @@
import OpenAI from 'openai';
import { SendLLMMessageFnTypeInternal } from './util';
import { parseMaxTokensStr } from './util.js';
// OpenAI, OpenRouter, OpenAICompatible
export const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter }) => {
let fullText = ''
let openai: OpenAI
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
const maxTokens = parseMaxTokensStr(voidConfig.default.maxTokens)
if (voidConfig.default.whichApi === 'openAI') {
const thisConfig = voidConfig.openAI
openai = new OpenAI({ apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true });
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openRouter') {
const thisConfig = voidConfig.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: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else if (voidConfig.default.whichApi === 'openAICompatible') {
const thisConfig = voidConfig.openAICompatible
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apikey, dangerouslyAllowBrowser: true })
options = { model: thisConfig.model, messages: messages, stream: true, max_completion_tokens: maxTokens }
}
else {
console.error(`sendOpenAIMsg: invalid whichApi: ${voidConfig.default.whichApi}`)
throw new Error(`voidConfig.whichAPI was invalid: ${voidConfig.default.whichApi}`)
}
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({ error: 'Invalid API key.' });
}
else {
onError({ error });
}
})
};
@@ -0,0 +1,109 @@
import { SendLLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
import { IMetricsService } from '../../common/metricsService.js';
import { sendAnthropicMsg } from './anthropic.js';
import { sendGeminiMsg } from './gemini.js';
import { sendGreptileMsg } from './greptile.js';
import { sendGroqMsg } from './groq.js';
import { sendOllamaMsg } from './ollama.js';
import { sendOpenAIMsg } from './openai.js';
export const sendLLMMessage = (
{
messages, onText: onText_, onFinalMessage: onFinalMessage_, onError: onError_,
abortRef: abortRef_, voidConfig, logging: { loggingName },
}: SendLLMMMessageParams,
metricsService: IMetricsService
) => {
if (!voidConfig) return;
// 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, {
whichApi: voidConfig.default['whichApi'],
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 = ({ error }) => {
if (_didAbort) return
console.error('sendLLMMessage onError:', error)
captureChatEvent(`${loggingName} - Error`, { error })
onError_({ error })
}
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 (voidConfig.default.whichApi) {
case 'anthropic':
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'openAI':
case 'openRouter':
case 'openAICompatible':
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'gemini':
sendGeminiMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'ollama':
sendOllamaMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'greptile':
sendGreptileMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
case 'groq':
sendGroqMsg({ messages, onText, onFinalMessage, onError, voidConfig, _setAborter, });
break;
default:
onError({ error: `Error: whichApi was "${voidConfig.default.whichApi}", which is not recognized!` })
break;
}
}
catch (error) {
if (error instanceof Error) { onError({ error }) }
else { onError({ error: `Unexpected Error in sendLLMMessage: ${error}` }); }
// ; (_aborter as any)?.()
// _didAbort = true
}
}
@@ -0,0 +1,21 @@
import { VoidConfig } from '../../common/configTypes'
import { LLMMessage, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes'
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 type SendLLMMessageFnTypeInternal = (params: {
messages: LLMMessage[];
onText: OnText;
onFinalMessage: OnFinalMessage;
onError: OnError;
voidConfig: VoidConfig;
_setAborter: (aborter: () => void) => void;
}) => void
@@ -0,0 +1,95 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 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 { listenerNames, ProxyOnTextPayload, ProxyOnErrorPayload, ProxyOnFinalMessagePayload, ProxyLLMMessageParams, AbortRef, SendLLMMMessageParams, ProxyLLMMessageAbortParams } from '../common/llmMessageTypes.js';
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
import { IMetricsService } from '../common/metricsService.js';
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
export class LLMMessageChannel implements IServerChannel {
private readonly _onText = new Emitter<ProxyOnTextPayload>();
readonly onText = this._onText.event;
private readonly _onFinalMessage = new Emitter<ProxyOnFinalMessagePayload>();
readonly onFinalMessage = this._onFinalMessage.event;
private readonly _onError = new Emitter<ProxyOnErrorPayload>();
readonly onError = this._onError.event;
private readonly _abortRefOfRequestId: Record<string, AbortRef> = {}
constructor(
private readonly metricsService: IMetricsService
) { }
// browser uses this to listen for changes
listen(_: unknown, event: typeof listenerNames[number]): Event<any> {
if (event === 'onText') {
return this.onText;
}
else if (event === 'onFinalMessage') {
return this.onFinalMessage;
}
else if (event === 'onError') {
return this.onError;
}
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 {
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 _callSendLLMMessage(params: ProxyLLMMessageParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId))
this._abortRefOfRequestId[requestId] = { current: null }
const mainThreadParams: SendLLMMMessageParams = {
...params,
onText: ({ newText, fullText }) => { this._onText.fire({ requestId, newText, fullText }); },
onFinalMessage: ({ fullText }) => { this._onFinalMessage.fire({ requestId, fullText }); },
onError: ({ error }) => { this._onError.fire({ requestId, error }); },
abortRef: this._abortRefOfRequestId[requestId],
}
sendLLMMessage(mainThreadParams, this.metricsService);
}
private _callAbort(params: ProxyLLMMessageAbortParams) {
const { requestId } = params;
if (!(requestId in this._abortRefOfRequestId)) return
this._abortRefOfRequestId[requestId].current?.()
delete this._abortRefOfRequestId[requestId]
}
}
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 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.init('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', { api_host: 'https://us.i.posthog.com', })
// const buildEnv = 'development';
// const buildNumber = '1.0.0';
// const isMac = process.platform === 'darwin';
// // TODO use commandKey
// const commandKey = isMac ? '⌘' : 'Ctrl';
// const systemInfo = {
// buildEnv,
// buildNumber,
// isMac,
// }
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) => {
console.log('Capturing', { event, params })
console.log('full capture:', { distinctId: this._distinctId, event, properties: params })
this.client.capture({ distinctId: this._distinctId, event, properties: params })
}
}
@@ -741,8 +741,24 @@ export class CodeWindow extends BaseWindow implements ICodeWindow {
cb({ cancel: false, requestHeaders: Object.assign(details.requestHeaders, headers) });
});
// // Void: send from https://
// this._win.webContents.session.webRequest.onBeforeSendHeaders({ urls }, async (details, cb) => {
// // const voidConfig = this.voidConfigStateService.state.voidConfig
// // const whichApi = voidConfig.default['whichApi']
// const endpoint = 'http://127.' //string | undefined = voidConfig[whichApi as VoidConfigField].endpoint
// if (endpoint && details.url.startsWith(endpoint)) {
// details.requestHeaders['Origin'] = 'https://app.voideditor.com'
// }
// cb({ cancel: false, requestHeaders: details.requestHeaders });
// });
}
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);
@@ -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();
}
}
}
@@ -0,0 +1 @@
void-imports/
@@ -0,0 +1,276 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { diffLines } from './react/out/util/diffLines.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,18 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import { OperatingSystem, OS } from '../../../../base/common/platform.js';
export function getCmdKey(): string {
if (OS === OperatingSystem.Macintosh) {
return '⌘';
} else {
return 'Ctrl';
}
}
@@ -0,0 +1,15 @@
.monaco-editor .void-sweepIdxBG {
background-color: var(--vscode-void-sweepIdxBG);
}
.void-sweepBG {
background-color: var(--vscode-void-sweepBG);
}
.void-greenBG {
background-color: var(--vscode-void-greenBG);
}
.void-redBG {
background-color: var(--vscode-void-redBG);
}
@@ -0,0 +1,25 @@
import { CodeSelection } from '../registerThreads.js';
export const filesStr = (selections: CodeSelection[]) => {
return selections.map(({ fileURI, content, selectionStr }) =>
`\
File: ${fileURI.fsPath}
\`\`\`
${content}
\`\`\`${selectionStr === null ? '' : `
Selection: ${selectionStr}`}
`).join('\n')
}
export const userInstructionsStr = (instructions: string, selections: CodeSelection[] | null) => {
let str = '';
if (selections && selections.length > 0) {
str += filesStr(selections);
str += `Please edit the selected code following these instructions:\n`
}
str += `${instructions}`;
return str;
};
@@ -1,5 +1,15 @@
const generateDiffInstructions = `
// // used for ctrl+l
// const partialGenerationInstructions = ``
// // used for ctrl+k, autocomplete
// const fimInstructions = ``
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).
@@ -160,7 +170,7 @@ We should change all the buttons like the one selected into a div component. Her
`;
const searchDiffChunkInstructions = `
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.
@@ -269,10 +279,10 @@ OUTPUT
`
const writeFileWithDiffInstructions = `
export const writeFileWithDiffInstructions = `
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\`.
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.
@@ -399,8 +409,3 @@ export default Sidebar;\`\`\`
export {
generateDiffInstructions,
searchDiffChunkInstructions,
writeFileWithDiffInstructions,
};
@@ -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).
+13
View File
@@ -0,0 +1,13 @@
import { execSync } from 'child_process';
// clear temp dirs
execSync('npx rimraf out/ && npx rimraf src2/')
// build and scope tailwind: https://www.npmjs.com/package/scope-tailwind
execSync('npx scope-tailwind ./src -o src2/ -s void-scope -c styles.css -p "prefix-" ')
// tsup to build src2/ into out/
execSync('npx tsup')
console.log('✅ Done building! Kill your build script(s) (Ctrl+D in them), then press Cmd+Shift+B again.')
@@ -1,9 +1,9 @@
import React, { ReactNode, useCallback, useEffect, useState } from "react"
import React, { ReactNode } from "react"
import SyntaxHighlighter from "react-syntax-highlighter";
import { atomOneDarkReasonable } from "react-syntax-highlighter/dist/esm/styles/hljs";
const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
export const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOnHover?: ReactNode, language?: string }) => {
const customStyle = {
...atomOneDarkReasonable,
@@ -16,9 +16,9 @@ const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOn
return (<>
<div className={`relative group w-full bg-vscode-sidebar-bg overflow-hidden isolate`}>
{!toolbar ? null : (
{buttonsOnHover === null ? null : (
<div className="absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200">
<div className="flex space-x-2 p-2">{buttonsOnHover === null ? null : buttonsOnHover}</div>
<div className="flex space-x-2 p-2">{buttonsOnHover}</div>
</div>
)}
@@ -39,4 +39,3 @@ const BlockCode = ({ text, buttonsOnHover, language }: { text: string, buttonsOn
)
}
export default BlockCode
@@ -1,19 +1,23 @@
import React, { JSX, useCallback, useEffect, useState } from "react"
import { marked, MarkedToken, Token, TokensList } from "marked"
import BlockCode from "./BlockCode"
import { getVSCodeAPI } from "../../common/getVscodeApi"
import React, { JSX, useCallback, useEffect, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { useService } from '../util/services.js'
enum CopyButtonState {
Copy = "Copy",
Copied = "Copied!",
Error = "Could not copy",
Copy = 'Copy',
Copied = 'Copied!',
Error = 'Could not copy',
}
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
const [copyButtonState, setCopyButtonState] = useState(CopyButtonState.Copy)
const inlineDiffService = useService('inlineDiffService')
const clipboardService = useService('clipboardService')
useEffect(() => {
if (copyButtonState !== CopyButtonState.Copy) {
@@ -24,15 +28,10 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
}, [copyButtonState])
const onCopy = useCallback(() => {
navigator.clipboard.writeText(text).then(
() => {
setCopyButtonState(CopyButtonState.Copied)
},
() => {
setCopyButtonState(CopyButtonState.Error)
}
)
}, [text])
clipboardService.writeText(text)
.then(() => { setCopyButtonState(CopyButtonState.Copied) })
.catch(() => { setCopyButtonState(CopyButtonState.Error) })
}, [text, clipboardService])
return <>
<button
@@ -44,7 +43,8 @@ const CodeButtonsOnHover = ({ diffRepr: text }: { diffRepr: string }) => {
<button
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
onClick={async () => {
getVSCodeAPI().postMessage({ type: "applyChanges", diffRepr: text })
inlineDiffService.startStreaming('ctrl+l', text)
}}
>
Apply
@@ -209,7 +209,7 @@ const RenderToken = ({ token, nested = false }: { token: Token | string, nested?
)
}
const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
export const MarkdownRender = ({ string, nested = false }: { string: string, nested?: boolean }) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
@@ -220,4 +220,3 @@ const MarkdownRender = ({ string, nested = false }: { string: string, nested?: b
)
}
export default MarkdownRender
@@ -0,0 +1,159 @@
import React, { useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
// const opaqueMessage = `\
// Unfortunately, Void can't see the full error. However, you should be able to find more details by pressing ${getCmdKey()}+Shift+P, typing "Toggle Developer Tools", and looking at the console.\n
// This error often means you have an incorrect API key. If you're self-hosting your own server, it might mean your CORS headers are off, and you should make sure your server's response has the header "Access-Control-Allow-Origins" set to "*", or at least allows "vscode-file://vscode-app".`
// if ((error instanceof Error) && (error.cause + '').includes('TypeError: Failed to fetch')) {
// e = error as any
// e['Void Team'] = opaqueMessage
// }
type Details = {
message: string,
name: string,
stack: string | null,
cause: string | null,
code: string | null,
additional: Record<string, any>
}
// Get detailed error information
const getErrorDetails = (error: unknown) => {
let details: Details;
let e: Error & { [other: string]: undefined | any }
// If fetch() fails, it gives an opaque message. We add extra details to the error.
if (error instanceof Error) {
e = error
}
// sometimes error is an object but not an Error
else if (typeof error === 'object') {
e = new Error(`More details below.`, { cause: JSON.stringify(error) })
}
else {
e = new Error(String(error))
}
// console.log('error display', JSON.stringify(e))
const message = e.message && e.error ?
(e.message + ':\n' + e.error)
: e.message || e.error || JSON.stringify(error)
details = {
name: e.name || 'Error',
message: message,
stack: null, // e.stack is ignored because it's ugly and not very useful
cause: e.cause ? String(e.cause) : null,
code: e.code || null,
additional: {}
}
// Collect any additional properties from the e
for (let prop of Object.getOwnPropertyNames(e).filter((prop) => !Object.keys(details).includes(prop)))
details.additional[prop] = (e as any)[prop]
return details;
};
export const ErrorDisplay = ({
error,
onDismiss = null,
showDismiss = true,
}: {
error: Error | object | string,
onDismiss: (() => void) | null,
showDismiss?: boolean,
className?: string
}) => {
const [isExpanded, setIsExpanded] = useState(false);
const details = getErrorDetails(error);
const hasDetails = details.cause || Object.keys(details.additional).length > 0;
return (
<div className={`rounded-lg border border-red-200 bg-red-50 p-4 overflow-auto`}>
{/* Header */}
<div className="flex items-start justify-between">
<div className="flex gap-3">
<AlertCircle className="h-5 w-5 text-red-500 mt-0.5" />
<div className="flex-1">
<h3 className="font-semibold text-red-800">
{details.name}
</h3>
<p className="text-red-700 mt-1">
{details.message}
</p>
</div>
</div>
<div className="flex gap-2">
{hasDetails && (
<button
onClick={() => setIsExpanded(!isExpanded)}
className="text-red-600 hover:text-red-800 p-1 rounded"
>
{isExpanded ? (
<ChevronUp className="h-5 w-5" />
) : (
<ChevronDown className="h-5 w-5" />
)}
</button>
)}
{showDismiss && onDismiss && (
<button
onClick={onDismiss}
className="text-red-600 hover:text-red-800 p-1 rounded"
>
<X className="h-5 w-5" />
</button>
)}
</div>
</div>
{/* Expandable Details */}
{isExpanded && hasDetails && (
<div className="mt-4 space-y-3 border-t border-red-200 pt-3">
{details.code && (
<div>
<span className="font-semibold text-red-800">Error Code: </span>
<span className="text-red-700">{details.code}</span>
</div>
)}
{details.cause && (
<div>
<span className="font-semibold text-red-800">Cause: </span>
<span className="text-red-700">{details.cause}</span>
</div>
)}
{Object.keys(details.additional).length > 0 && (
<div>
<span className="font-semibold text-red-800">Additional Information:</span>
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
{Object.keys(details.additional).map(key => `${key}:\n${details.additional[key]}`).join('\n')}
</pre>
</div>
)}
{/* {details.stack && (
<div>
<span className="font-semibold text-red-800">Stack Trace:</span>
<pre className="mt-1 text-sm text-red-700 overflow-x-auto whitespace-pre-wrap">
{details.stack}
</pre>
</div>
)} */}
</div>
)}
</div>
);
};
@@ -0,0 +1,52 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { SidebarSettings } from './SidebarSettings.js';
import { useSidebarState } from '../util/services.js';
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
import '../styles.css'
import { SidebarThreadSelector } from './SidebarThreadSelector.js';
import { SidebarChat } from './SidebarChat.js';
const Sidebar = () => {
const sidebarState = useSidebarState()
const { isHistoryOpen, currentTab: tab } = sidebarState
// className='@@void-scope'
return <div className='@@void-scope'>
<div className={`flex flex-col w-full px-2 py-2`}>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
const index = tabs.indexOf(tab)
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
<div className={`mb-2 ${isHistoryOpen ? '' : 'hidden'}`}>
<SidebarThreadSelector />
</div>
<div className={`${tab === 'chat' ? '' : 'hidden'}`}>
<SidebarChat />
</div>
<div className={`${tab === 'settings' ? '' : 'hidden'}`}>
<SidebarSettings />
</div>
</div>
</div>
}
const mountFn = mountFnGenerator(Sidebar)
export default mountFn
@@ -0,0 +1,349 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { FormEvent, Fragment, useCallback, useEffect, useRef, useState } from 'react';
import { useConfigState, useService, useThreadsState } from '../util/services.js';
import { generateDiffInstructions } from '../../../prompt/systemPrompts.js';
import { userInstructionsStr } from '../../../prompt/stringifyFiles.js';
import { ChatMessage, CodeSelection, CodeStagingSelection } from '../../../registerThreads.js';
import { BlockCode } from '../markdown/BlockCode.js';
import { MarkdownRender } from '../markdown/MarkdownRender.js';
import { IModelService } from '../../../../../../../editor/common/services/model.js';
import { URI } from '../../../../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../../../../editor/common/model.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { LLMMessageServiceParams } from '../../../../../../../platform/void/common/llmMessageTypes.js';
import { getCmdKey } from '../../../getCmdKey.js'
import { HistoryInputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { VoidInputBox } from './inputs.js';
// read files from VSCode
const VSReadFile = async (modelService: IModelService, uri: URI): Promise<string | null> => {
const model = modelService.getModel(uri)
if (!model) return null
return model.getValue(EndOfLinePreference.LF)
}
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 = (
{ type, selections, setStaging }:
| { type: 'past', selections: CodeSelection[] | null; setStaging?: undefined }
| { type: 'staging', selections: CodeStagingSelection[] | null; setStaging: ((files: CodeStagingSelection[]) => void) }
) => {
return (
!!selections && selections.length !== 0 && (
<div className='flex flex-wrap -mx-1 -mb-1'>
{selections.map((selection, i) => (
<Fragment key={i}>
<button
disabled={!setStaging}
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={() => {
if (type !== 'staging') return
setStaging([...selections.slice(0, i), ...selections.slice(i + 1, Infinity)])
}}
>
<span>{getBasename(selection.fileURI.fsPath)}</span>
{/* X button */}
{type === 'staging' && <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>
{/* selection text */}
{type === 'staging' && selection.selectionStr && <BlockCode text={selection.selectionStr}
buttonsOnHover={(<button
onClick={() => {
setStaging([...selections.slice(0, i), { ...selection, selectionStr: null }, ...selections.slice(i + 1, Infinity)])
}}
className="btn btn-secondary btn-sm border border-vscode-input-border rounded"
>Remove</button>
)} />}
</Fragment>
))}
</div>
)
)
}
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 type='past' selections={chatMessage.selections} />
{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>
}
export const SidebarChat = () => {
const chatInputRef = useRef<HTMLTextAreaElement | null>(null)
const modelService = useService('modelService')
// ----- HIGHER STATE -----
// sidebar state
const sidebarStateService = useService('sidebarStateService')
useEffect(() => {
const disposables: IDisposable[] = []
disposables.push(
sidebarStateService.onDidFocusChat(() => { chatInputRef.current?.focus() }),
sidebarStateService.onDidBlurChat(() => { chatInputRef.current?.blur() })
)
return () => disposables.forEach(d => d.dispose())
}, [sidebarStateService, chatInputRef])
// config state
const configState = useConfigState()
const { voidConfig } = configState
// threads state
const threadsState = useThreadsState()
const threadsStateService = useService('threadsStateService')
// ----- SIDEBAR CHAT state (local) -----
// state of chat
const [messageStream, setMessageStream] = useState<string | null>(null)
const [isLoading, setIsLoading] = useState(false)
const latestRequestIdRef = useRef<string | null>(null)
const [latestError, setLatestError] = useState<Error | string | null>(null)
const sendLLMMessageService = useService('sendLLMMessageService')
// state of current message
const [instructions, setInstructions] = useState('') // the user's instructions
const onChangeText = useCallback((newStr: string) => { setInstructions(newStr) }, [setInstructions])
const isDisabled = !instructions
const formRef = useRef<HTMLFormElement | null>(null)
const inputBoxRef: React.MutableRefObject<HistoryInputBox | null> = useRef(null);
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
e.preventDefault()
if (isDisabled) return
if (isLoading) return
const currSelns = threadsStateService.state._currentStagingSelections
const selections = !currSelns ? null : await Promise.all(
currSelns.map(async (sel) => ({ ...sel, content: await VSReadFile(modelService, sel.fileURI) }))
).then(
(files) => files.filter(file => file.content !== null) as CodeSelection[]
)
// add system message to chat history
const systemPromptElt: ChatMessage = { role: 'system', content: generateDiffInstructions }
threadsStateService.addMessageToCurrentThread(systemPromptElt)
const userContent = userInstructionsStr(instructions, selections)
const userHistoryElt: ChatMessage = { role: 'user', content: userContent, displayContent: instructions, selections }
threadsStateService.addMessageToCurrentThread(userHistoryElt)
const currentThread = threadsStateService.getCurrentThread(threadsStateService.state) // the the instant state right now, don't wait for the React state
// send message to LLM
const object: LLMMessageServiceParams = {
logging: { loggingName: 'Chat' },
messages: [...(currentThread?.messages ?? []).map(m => ({ role: m.role, content: m.content || '(null)' })),],
onText: ({ newText, fullText }) => setMessageStream(fullText),
onFinalMessage: ({ fullText: content }) => {
console.log('chat: running final message')
// add assistant's message to chat history, and clear selection
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream(null)
setIsLoading(false)
},
onError: ({ error }) => {
console.log('chat: running error', error)
// add assistant's message to chat history, and clear selection
let content = messageStream ?? ''; // just use the current content
const assistantHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content || null, }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream('')
setIsLoading(false)
setLatestError(error)
},
voidConfig,
}
const latestRequestId = sendLLMMessageService.sendLLMMessage(object)
latestRequestIdRef.current = latestRequestId
setIsLoading(true)
if (inputBoxRef.current) {
inputBoxRef.current.value = ''; // this triggers onDidChangeText
inputBoxRef.current.blur();
}
threadsStateService.setStaging([]) // clear staging
setLatestError(null)
}
const onAbort = () => {
// abort the LLM
if (latestRequestIdRef.current)
sendLLMMessageService.abort(latestRequestIdRef.current)
// if messageStream was not empty, add it to the history
const llmContent = messageStream ?? ''
const assistantHistoryElt: ChatMessage = { role: 'assistant', content: llmContent, displayContent: messageStream || null, }
threadsStateService.addMessageToCurrentThread(assistantHistoryElt)
setMessageStream('')
setIsLoading(false)
}
const currentThread = threadsStateService.getCurrentThread(threadsState)
const selections = threadsState._currentStagingSelections
return <>
<div className="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 || null }} />
</div>
{/* chatbar */}
<div className="shrink-0 py-4">
{/* selection */}
<div className="text-left">
<div className="relative">
<div className="input">
{/* selections */}
{(selections && selections.length !== 0) && <div className="p-2 pb-0 space-y-2">
<SelectedFiles type='staging' selections={selections} setStaging={threadsStateService.setStaging.bind(threadsStateService)} />
</div>}
{/* error message */}
{latestError === null ? null :
<ErrorDisplay
error={latestError}
onDismiss={() => { setLatestError(null) }}
/>}
<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!')
onSubmit(e)
}}>
{/* input */}
<VoidInputBox
placeholder={`${getCmdKey()}+L to select`}
onChangeText={onChangeText}
inputBoxRef={inputBoxRef}
multiline={true}
initVal=''
/>
{/* <textarea
ref={chatInputRef}
placeholder={`Press ${getCmdKey()}+L to select.`}
onChange={(e) => { setInstructions(e.target.value) }}
className={`w-full p-2 leading-tight resize-none max-h-[50vh] overflow-auto bg-transparent border-none !outline-none`}
rows={1}
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
/> */}
{isLoading ?
// stop button
<button
onClick={onAbort}
type='button'
className="font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
>
<svg
className='scale-50'
stroke="currentColor" fill="currentColor" strokeWidth="0" viewBox="0 0 24 24" height="24" width="24" xmlns="http://www.w3.org/2000/svg">
<path d="M24 24H0V0h24v24z"></path>
</svg>
</button>
:
// submit button (up arrow)
<button
className="font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
disabled={isDisabled}
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>
</>
}
@@ -0,0 +1,150 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useConfigState, useService } from '../util/services.js';
import { HistoryInputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
import { ConfigState, IVoidConfigStateService } from '../../../registerConfig.js';
import { nonDefaultConfigFields, VoidConfigField } from '../../../../../../../platform/void/common/configTypes.js';
const SettingOfFieldAndParam = ({ field, param, configState, configStateService }:
{ field: VoidConfigField; param: string; configState: ConfigState; configStateService: IVoidConfigStateService }) => {
const { partialVoidConfig } = configState
const { enumArr, defaultVal, description } = configStateService.voidConfigInfo[field][param]
const val = partialVoidConfig[field]?.[param] ?? defaultVal // current value of this item
const initValRef = useRef(val)
const updateState = useCallback((newValue: string) => {
configStateService.setField(field, param, newValue)
}, [configStateService, field, param])
const inputBoxRef = useRef<HistoryInputBox | null>(null);
const selectBoxRef = useRef<SelectBox | null>(null);
const forceState = useCallback((newValue: string) => {
if (inputBoxRef.current) {
inputBoxRef.current.value = newValue;
}
if (selectBoxRef.current) {
selectBoxRef.current.select(enumArr?.indexOf(newValue) ?? 0);
}
// updateState is called automatically when the change happens
}, [enumArr, updateState])
const resetButton = <button
disabled={val === defaultVal}
title={val === defaultVal ? 'This is the default value.' : `Revert value to '${defaultVal}'?`}
className='group btn btn-sm disabled:opacity-75 disabled:cursor-default'
onClick={() => forceState(defaultVal)}
>
<svg
className='size-5 group-disabled:stroke-current group-disabled:fill-current group-hover:stroke-red-600 group-hover:fill-red-600 duration-200'
fill='currentColor' strokeWidth='0' viewBox='0 0 16 16' height='200px' width='200px' xmlns='http://www.w3.org/2000/svg'><path fillRule='evenodd' clipRule='evenodd' d='M3.5 2v3.5L4 6h3.5V5H4.979l.941-.941a3.552 3.552 0 1 1 5.023 5.023L5.746 14.28l.72.72 5.198-5.198A4.57 4.57 0 0 0 5.2 3.339l-.7.7V2h-1z'></path>
</svg>
</button>
const inputElement = enumArr === undefined ?
// string
// (<VoidInputBox
// onChangeText={updateState}
// initVal={initValRef.current}
// multiline={false}
// placeholder=''
// inputBoxRef={inputBoxRef}
// />)
<input
className='input p-1 w-full'
type='text'
value={val}
onChange={(e) => updateState(e.target.value)}
/>
:
// enum
// (<VoidSelectBox
// onChangeSelection={updateState}
// initVal={initValRef.current}
// options={enumArr}
// selectBoxRef={selectBoxRef}
// />)
(<select
className='dropdown p-1 w-full'
value={val}
onChange={(e) => updateState(e.target.value)}
>
{enumArr.map((option) => (
<option key={option} value={option}>
{option}
</option>
))}
</select>)
return <div>
<label className='hidden'>{param}</label>
<span>{description}</span>
<div className='flex items-center'>
{inputElement}
{resetButton}
</div>
</div>
}
export const SidebarSettings = () => {
const configState = useConfigState()
const configStateService = useService('configStateService')
const { voidConfig } = configState
const current_field = voidConfig.default['whichApi'] as VoidConfigField
return (
<div className='space-y-4 py-2 overflow-y-auto'>
{/* choose the field */}
<div className='outline-vscode-input-bg'>
<SettingOfFieldAndParam
configState={configState}
configStateService={configStateService}
field='default'
param='whichApi'
/>
<SettingOfFieldAndParam
configState={configState}
configStateService={configStateService}
field='default'
param='maxTokens'
/>
</div>
<hr />
{/* render all fields, but hide the ones not visible for fast tab switching */}
{nonDefaultConfigFields.map(field => {
return <div
key={field}
className={`flex flex-col gap-y-2 ${field !== current_field ? 'hidden' : ''}`}
>
{Object.keys(configStateService.voidConfigInfo[field]).map((param) => (
<SettingOfFieldAndParam
key={param}
configState={configState}
configStateService={configStateService}
field={field}
param={param}
/>
))}
</div>
})}
</div>
)
}
@@ -0,0 +1,90 @@
/*---------------------------------------------------------------------------------------------
* Copyright (c) Glass Devtools, Inc. All rights reserved.
* Void Editor additions licensed under the AGPLv3 License.
*--------------------------------------------------------------------------------------------*/
import React from "react";
import { useService, useThreadsState } from '../util/services.js';
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
export const SidebarThreadSelector = () => {
const threadsState = useThreadsState()
const threadsStateService = useService('threadsStateService')
const sidebarStateService = useService('sidebarStateService')
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {}).sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
return (
<div className="flex flex-col gap-y-1 overflow-y-auto h-[30vh]">
{/* X button at top right */}
<div className="text-right">
<button className="btn btn-sm" onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}>
<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>
{/* a list of all the past threads */}
<div className='flex flex-col gap-y-1 max-h-80 overflow-y-auto'>
{sortedThreadIds.map((threadId) => {
if (!allThreads)
return <>Error: Threads not found.</>
const pastThread = allThreads[threadId]
let btnStringArr: string[] = []
const firstMsgIdx = allThreads[threadId].messages.findIndex(msg => msg.role !== 'system' && !!msg.displayContent) ?? ''
if (firstMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[firstMsgIdx].displayContent ?? ''))
else
btnStringArr.push('""')
const secondMsgIdx = allThreads[threadId].messages.findIndex((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx) ?? ''
if (secondMsgIdx !== -1)
btnStringArr.push(truncate(allThreads[threadId].messages[secondMsgIdx].displayContent ?? ''))
const numMessagesRemaining = allThreads[threadId].messages.filter((msg, i) => msg.role !== 'system' && !!msg.displayContent && i > secondMsgIdx).length
if (numMessagesRemaining > 0)
btnStringArr.push(numMessagesRemaining + '')
const btnString = btnStringArr.join(' / ')
return (
<button
key={pastThread.id}
className={`rounded-sm`}
onClick={() => threadsStateService.switchToThread(pastThread.id)}
title={new Date(pastThread.createdAt).toLocaleString()}
>
{btnString}
</button>
)
})}
</div>
</div>
)
}
@@ -0,0 +1,104 @@
import React, { useEffect, useRef } from 'react';
import { useService } from '../util/services.js';
import { HistoryInputBox, InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js';
import { defaultInputBoxStyles } from '../../../../../../../platform/theme/browser/defaultStyles.js';
import { SelectBox, unthemedSelectBoxStyles } from '../../../../../../../base/browser/ui/selectBox/selectBox.js';
export const VoidInputBox = ({ onChangeText, initVal, placeholder, inputBoxRef, multiline }: {
onChangeText: (value: string) => void;
placeholder: string;
inputBoxRef: React.MutableRefObject<InputBox | null>;
multiline: boolean;
initVal: string;
}) => {
const contextViewProvider = useService('contextViewService');
const containerRef = useRef<HTMLDivElement>(null);
useEffect(() => {
if (!containerRef.current) return;
// create and mount the HistoryInputBox
inputBoxRef.current = new InputBox(
containerRef.current,
contextViewProvider,
{
inputBoxStyles: {
...defaultInputBoxStyles,
inputBackground: 'transparent',
},
placeholder,
flexibleHeight: multiline,
flexibleMaxHeight: 500,
flexibleWidth: false,
}
);
inputBoxRef.current.value = initVal;
inputBoxRef.current.onDidChange((newStr) => {
console.log('CHANGE TEXT on inputbox', newStr)
onChangeText(newStr)
})
// cleanup
return () => {
if (inputBoxRef.current) {
inputBoxRef.current.dispose();
if (containerRef.current) {
while (containerRef.current.firstChild) {
containerRef.current.removeChild(containerRef.current.firstChild);
}
}
inputBoxRef.current = null;
}
};
}, [inputBoxRef, contextViewProvider, placeholder, multiline, initVal, onChangeText]);
return <div ref={containerRef} className="w-full" />;
};
// export const VoidSelectBox = ({ onChangeSelection, initVal, selectBoxRef, options }: {
// onChangeSelection: (value: string) => void;
// initVal: string;
// selectBoxRef: React.MutableRefObject<SelectBox | null>;
// options: readonly string[];
// }) => {
// const containerRef = useRef<HTMLDivElement>(null);
// const contextViewProvider = useService('contextViewService');
// useEffect(() => {
// if (!containerRef.current) return;
// const defaultIndex = options.indexOf(initVal);
// selectBoxRef.current = new SelectBox(
// options.map(opt => ({ text: opt })),
// defaultIndex,
// contextViewProvider,
// unthemedSelectBoxStyles
// );
// selectBoxRef.current.render(containerRef.current);
// selectBoxRef.current.onDidSelect(e => { onChangeSelection(e.selected); });
// // cleanup
// return () => {
// if (selectBoxRef.current) {
// selectBoxRef.current.dispose();
// if (containerRef.current) {
// while (containerRef.current.firstChild) {
// containerRef.current.removeChild(containerRef.current.firstChild);
// }
// }
// }
// };
// }, [options, initVal, onChangeSelection, contextViewProvider, selectBoxRef]);
// return <div ref={containerRef} className="w-full" />;
// };
@@ -1,10 +1,9 @@
/* all the styles are shared right now between all webviews */
@tailwind base;
@tailwind components;
@tailwind utilities;
html {
/* html {
font-size: var(--vscode-font-size);
}
@@ -31,7 +30,6 @@ html {
}
}
/* add transparency when disabled */
&:disabled {
@apply opacity-75 cursor-not-allowed;
}
@@ -43,4 +41,4 @@ html {
.dropdown {
@apply bg-vscode-dropdown-bg text-vscode-dropdown-foreground border-vscode-dropdown-border focus:outline-vscode-focus-border;
}
} */
@@ -0,0 +1,3 @@
import { diffLines, Change } from 'diff';
export { diffLines, Change }
@@ -0,0 +1,17 @@
import React, { useEffect, useState } from 'react';
import * as ReactDOM from 'react-dom/client'
import { ReactServicesType, VoidSidebarState } from '../../../registerSidebar.js';
import { _registerServices } from './services.js';
export const mountFnGenerator = (Component: React.FC) => (rootElement: HTMLElement, services: ReactServicesType) => {
if (typeof document === 'undefined') {
console.error('index.tsx error: document was undefined')
return
}
_registerServices(services)
const root = ReactDOM.createRoot(rootElement)
root.render(<Component />);
}
@@ -0,0 +1,93 @@
import { useState, useEffect } from 'react'
import { ConfigState } from '../../../registerConfig.js'
import { VoidSidebarState, ReactServicesType } from '../../../registerSidebar.js'
import { ThreadsState } from '../../../registerThreads.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
let services: ReactServicesType
// even if React hasn't mounted yet, these variables are always updated to the latest state:
let sidebarState: VoidSidebarState
let configState: ConfigState
let threadsState: ThreadsState
// React listens by adding a setState function to these:
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
const configStateListeners: Set<(s: ConfigState) => void> = new Set()
const threadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
// must call this before you can use any of the hooks below
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
let wasCalled = false
export const _registerServices = (services_: ReactServicesType) => {
if (wasCalled) console.error(`void _registerServices was called again! It should only be called once.`)
wasCalled = true
services = services_
const { sidebarStateService, configStateService, threadsStateService, } = services
sidebarState = sidebarStateService.state
sidebarStateService.onDidChangeState(() => {
sidebarState = sidebarStateService.state
sidebarStateListeners.forEach(l => l(sidebarState))
})
configState = configStateService.state
configStateService.onDidChangeState(() => {
configState = configStateService.state
configStateListeners.forEach(l => l(configState))
})
threadsState = threadsStateService.state
threadsStateService.onDidChangeCurrentThread(() => {
threadsState = threadsStateService.state
threadsStateListeners.forEach(l => l(threadsState))
})
}
// -- services --
export const useService = <T extends keyof ReactServicesType,>(serviceName: T) => {
if (services === null) {
throw new Error('useAccessor must be used within an AccessorProvider')
}
return services[serviceName] as ReactServicesType[T]
}
// -- state of services --
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {
ss(sidebarState)
sidebarStateListeners.add(ss)
return () => { sidebarStateListeners.delete(ss) }
}, [ss])
return s
}
export const useConfigState = () => {
const [s, ss] = useState(configState)
useEffect(() => {
ss(configState)
configStateListeners.add(ss)
return () => { configStateListeners.delete(ss) }
}, [ss])
return s
}
export const useThreadsState = () => {
const [s, ss] = useState(threadsState)
useEffect(() => {
ss(threadsState)
threadsStateListeners.add(ss)
return () => { threadsStateListeners.delete(ss) }
}, [ss])
return s
}
@@ -1,10 +1,9 @@
/** @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/webviews/**/*.{html,js,ts,jsx,tsx}"],
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
theme: {
extend: {
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
colors: {
vscode: {
"sidebar-bg": "var(--vscode-sideBar-background)",
@@ -28,4 +27,6 @@ module.exports = {
},
},
plugins: [],
};
prefix: 'prefix-'
}
@@ -0,0 +1,16 @@
{
"compilerOptions": {
"strict": true,
"exactOptionalPropertyTypes": false,
"jsx": "react-jsx",
"moduleResolution": "NodeNext",
"module": "NodeNext",
"esModuleInterop": true,
},
"include": [
// this is just for type checking, so src/ is the correct dir
"./src/**/*.ts",
"./src/**/*.tsx"
]
}

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