Compare commits
914 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 7a7975af4d | |||
| 688b8588be | |||
| 24e276ba47 | |||
| 4e0dd8f8db | |||
| c239682ad8 | |||
| 3bfb50e6ee | |||
| 1b4943f39e | |||
| 8c443ab780 | |||
| 7519f05238 | |||
| c2fc2577e0 | |||
| 6e1f8a08b3 | |||
| d9be67d17d | |||
| 60d22674e2 | |||
| e8c0ee631c | |||
| 6ba9d10a4a | |||
| 4eff8f8e76 | |||
| 66e349c72e | |||
| 146d63c41e | |||
| 82439406a0 | |||
| efeb0d28b7 | |||
| aba7d7bf90 | |||
| f517dc79a9 | |||
| dc33d4d0c0 | |||
| d3647bf93c | |||
| 29c4603909 | |||
| 85426e28bd | |||
| c6ff563e1b | |||
| 7ec574eeee | |||
| 66c880945c | |||
| 6f1905a962 | |||
| 268d2ae99a | |||
| 1521038e03 | |||
| dce0378eb2 | |||
| ce7ee62abe | |||
| 015943329f | |||
| 4b15885147 | |||
| 2e3f608805 | |||
| 789016709a | |||
| 835a2bdf92 | |||
| c02f1de758 | |||
| ddd9cf857f | |||
| e4fd6d05a4 | |||
| 9756ab6d16 | |||
| 433884b36c | |||
| 0d48db1a0c | |||
| 10adc13b60 | |||
| 2103a95930 | |||
| f8c454544c | |||
| e3da020c0a | |||
| 7dec57ae04 | |||
| f8db386c3b | |||
| 2d583b87dc | |||
| b9dba7d08b | |||
| 4e18dd4ac3 | |||
| 471333ec16 | |||
| d43453e5ed | |||
| 3abafa76ec | |||
| e48e798afc | |||
| 4619e4146a | |||
| f522281c6d | |||
| 1e3668058b | |||
| 88c921b059 | |||
| 8cfe494632 | |||
| d4fc59034a | |||
| 9ba70cdc4a | |||
| 12d08eb709 | |||
| f3270a2219 | |||
| bd30f14980 | |||
| 725de600cf | |||
| ccfd54fcfe | |||
| 071c4893c2 | |||
| 8f15c67960 | |||
| fd546ff7b7 | |||
| 895628bb91 | |||
| 4898875532 | |||
| 9ddc22b63c | |||
| 44438fe1fe | |||
| 64e43fa53e | |||
| 07f4a59ce6 | |||
| c7d5d5d183 | |||
| 9f96ebcfc2 | |||
| 01839654d4 | |||
| 37200dacbd | |||
| 86612fd5ed | |||
| 937816519c | |||
| 35e366a5ba | |||
| d2fdbd7ffd | |||
| 33a003fc2d | |||
| 9589134830 | |||
| 164684a39b | |||
| accff821f8 | |||
| 00fc9964e5 | |||
| 3e5ead2547 | |||
| 52d89e5815 | |||
| 4cf13b67b0 | |||
| fc327e3fcb | |||
| 59afe2d6ff | |||
| 1d590510ae | |||
| c5a6f476ef | |||
| ae4ece1826 | |||
| ec2f04be91 | |||
| 8a56ea9755 | |||
| 261736bde5 | |||
| 7dd5171bd2 | |||
| becd109776 | |||
| 0d9f441967 | |||
| 6386fc95ee | |||
| 06688228ae | |||
| b3d2453e7f | |||
| 29e1f312c3 | |||
| b9b645522a | |||
| 83bf660b76 | |||
| 268f08c51d | |||
| 046942be75 | |||
| 6fed97eb83 | |||
| e888f261fe | |||
| 6e8498414c | |||
| bcda1dbe2b | |||
| be3128acb4 | |||
| 29cbb15911 | |||
| 8da71dc1b7 | |||
| a69b99b562 | |||
| 6868a8b1d8 | |||
| 10f8fcc17d | |||
| ee9f84a475 | |||
| c056214684 | |||
| fcf03f2710 | |||
| ed50ed5319 | |||
| f15f5ce8ed | |||
| 11578d6288 | |||
| d904dbb93e | |||
| 6df0c0ac69 | |||
| 0f98af7fc1 | |||
| e1b8e49b13 | |||
| e6f6c7500d | |||
| 951da8e31a | |||
| 966470cf68 | |||
| b2cf709685 | |||
| ab0bf4bef0 | |||
| db20584a19 | |||
| 7d1d638dae | |||
| 0d1c4944c1 | |||
| f7c4d28d55 | |||
| a2ae7a9674 | |||
| 262e15764b | |||
| 26f97f5939 | |||
| a9dc35e5db | |||
| 326dcae24f | |||
| f880ddd6dc | |||
| 7925e438c6 | |||
| 2f5e954ebf | |||
| 6a8ac1a68e | |||
| c5b6fd9b75 | |||
| 7e68a485f2 | |||
| a1da8bd12f | |||
| ebd78a902d | |||
| f96c03c846 | |||
| fed865e753 | |||
| 8d2917d331 | |||
| f4c1fc8917 | |||
| 0a3f31acae | |||
| 924c953697 | |||
| 446dede5e4 | |||
| 7bb0993249 | |||
| 4968452003 | |||
| 037f9f38e5 | |||
| e132c87792 | |||
| 07d87c8bf0 | |||
| c7ad7d3761 | |||
| ff9d21eee0 | |||
| e4953057ef | |||
| 865c34f282 | |||
| ec65903692 | |||
| a08e6078f2 | |||
| 20e1b287f8 | |||
| f2f9b6ccca | |||
| c4e4701dc3 | |||
| 37af31f958 | |||
| b5dff4cf0f | |||
| 3621bb63f4 | |||
| da247c6ed4 | |||
| ce634eadaa | |||
| 910ed7faa9 | |||
| 9e852355d9 | |||
| 8ae4700bb2 | |||
| f13c30ae0d | |||
| af244938b8 | |||
| a53f92432b | |||
| ca2b18d9da | |||
| 5293095eb8 | |||
| 86346f1bf0 | |||
| a0672f845d | |||
| f96d320ce0 | |||
| bb7e69b20b | |||
| f19e579ce9 | |||
| dd133d2cd5 | |||
| 2ba04a78a8 | |||
| fb948b331f | |||
| 5083b8e971 | |||
| 35091eb8f2 | |||
| b9e61bf4e2 | |||
| 55ec3365a2 | |||
| d6811e872a | |||
| c176b8020f | |||
| 85c2c80db0 | |||
| b677d3dfc1 | |||
| 6dcf684429 | |||
| 26ddd6418c | |||
| cc177b9cd4 | |||
| dd204978e5 | |||
| fb48c4e67e | |||
| 74fa5cb505 | |||
| fae03c3060 | |||
| 68722ee5c3 | |||
| cffe558cf5 | |||
| 9bc551f0ac | |||
| 01d7878671 | |||
| 2519f094cc | |||
| 4a993a716b | |||
| 23d1d4169a | |||
| f688e1ffae | |||
| a443315c14 | |||
| eeacb2d3c9 | |||
| cdbdf3c5c7 | |||
| 825c40bfff | |||
| a72305819a | |||
| a3d3309b9d | |||
| 32bfe9a111 | |||
| a9adcad560 | |||
| 80f7511eee | |||
| 7fe8f5c47d | |||
| b31882565b | |||
| 6aaf425419 | |||
| b0d21dfdb1 | |||
| 2087948789 | |||
| 6feb713682 | |||
| 667dc8bd7e | |||
| bdb0c65346 | |||
| df14b80d8b | |||
| 86decf8ed9 | |||
| bd9b9a9058 | |||
| 83c1f3430a | |||
| 02de8937ac | |||
| 78f3819c33 | |||
| b9cea4906e | |||
| 4a4677b5e2 | |||
| 5dab1a6a77 | |||
| 9b01d5ba7c | |||
| dcb708758d | |||
| dd4548466a | |||
| cda489e3e0 | |||
| 13594ef06c | |||
| c2b8df06eb | |||
| 33809ec48b | |||
| b4820abc9e | |||
| 0e79dec0d7 | |||
| 659f01413f | |||
| 110534df30 | |||
| d3b61867f0 | |||
| 954e611fda | |||
| 64760aae13 | |||
| d0d1124543 | |||
| 90a3fd0a21 | |||
| 11ed2545c7 | |||
| 3d1df92f6a | |||
| 4ca16caa4e | |||
| 06b5cca59a | |||
| e3c292eee7 | |||
| 99c3786d06 | |||
| 6e40a92315 | |||
| a9b2ff631d | |||
| 198adaaf18 | |||
| 2df7fa9ebc | |||
| 312942b004 | |||
| 0a444f6ff8 | |||
| 0c10cb73f6 | |||
| ef8bba00b9 | |||
| 3a3bc24fef | |||
| ff7830e704 | |||
| 01a345a391 | |||
| 33909b3143 | |||
| 9543bf8eb3 | |||
| 9a9d0da91e | |||
| d85a316536 | |||
| 1df044fb02 | |||
| 076e0add44 | |||
| dfbd729fa4 | |||
| 8a17c9246b | |||
| 1afb39b155 | |||
| f08fd04af5 | |||
| ef9d0daef4 | |||
| ffe5a41b01 | |||
| 372beb7a2d | |||
| 406f29bb5a | |||
| ee8b98ff74 | |||
| ffb0549c7c | |||
| 8f9c2f0630 | |||
| 84bfc26dce | |||
| 80a87ff3c6 | |||
| 806ec939e5 | |||
| 98a22d2eb6 | |||
| ffa31bc289 | |||
| a1ab8a5796 | |||
| 70c7d0261c | |||
| e7af8d9448 | |||
| e1feaba74a | |||
| 8456f979ba | |||
| 6630d596f6 | |||
| a435a59a2e | |||
| 9cf4980004 | |||
| 6298be394d | |||
| e647ce0054 | |||
| 5fb30fed87 | |||
| 7cc57bf086 | |||
| 3fb6a4890d | |||
| 97fd332c4e | |||
| 81282e16b3 | |||
| 382275fa9f | |||
| 91f8321278 | |||
| 9b1f21d7c1 | |||
| 739ac2562b | |||
| 298423928e | |||
| 702d11ed3e | |||
| 4513b732d6 | |||
| 78e72f0c80 | |||
| f457f006fb | |||
| d6074e7ab5 | |||
| 3669401819 | |||
| ee7d0ed836 | |||
| ba87b4f0de | |||
| e4bc21ffb1 | |||
| 81b78b7765 | |||
| c3c53b5e81 | |||
| f49a6f8e41 | |||
| 5a26aa8746 | |||
| 8661fe1417 | |||
| 2a519157da | |||
| ac8365def4 | |||
| 610b52587b | |||
| 01efab6a1d | |||
| 7f8e1ff00d | |||
| 5b55eed08a | |||
| d618223289 | |||
| 7752278720 | |||
| 597c04f63b | |||
| a7c4a1b179 | |||
| a7aab9cf86 | |||
| c44497510b | |||
| c485d2c478 | |||
| dfdf29fb36 | |||
| 420fb1a186 | |||
| 78a48bae13 | |||
| fc187e0028 | |||
| f973f8d4e7 | |||
| 8403eed8ca | |||
| 718b8cc61b | |||
| f6924df09b | |||
| 8b25f80e9b | |||
| a42ff2d59a | |||
| 0589a2a5ec | |||
| 7dda4bdc08 | |||
| f21ef61677 | |||
| 56b6d406d6 | |||
| 2ae29a50e5 | |||
| 9673e974ea | |||
| 842d388669 | |||
| f146385e1d | |||
| bb87726b34 | |||
| deb30b49bc | |||
| 56923eec52 | |||
| d2692d2594 | |||
| 5d44fdc17d | |||
| aada60c5be | |||
| b53e5d4886 | |||
| f299998994 | |||
| 967292e615 | |||
| 19e3ebefda | |||
| 7cf6c9d798 | |||
| 14e314e5b8 | |||
| 4e6e0b3bde | |||
| 63b086b786 | |||
| bda6ad06c9 | |||
| 829d82850d | |||
| 3991372f5b | |||
| 9b427c78bf | |||
| 5dad4c8f06 | |||
| e2d39fe4b1 | |||
| 1365400f21 | |||
| 94f4d2caaf | |||
| 696abc39fd | |||
| abc7151cd6 | |||
| c86f47743e | |||
| 9d6eb62c3e | |||
| 2f26b34a96 | |||
| 5807c94a61 | |||
| df23314bd2 | |||
| 6703aa1545 | |||
| aaab289ece | |||
| d809839530 | |||
| 9e583960ae | |||
| 19737d4cdc | |||
| 901579a445 | |||
| 051f8f38e7 | |||
| 07fcb3a72c | |||
| 2d80a29275 | |||
| 62cdaba44b | |||
| 3147a92463 | |||
| d175ac20ef | |||
| 42a2dec781 | |||
| dbef72c01c | |||
| 6ddf977c5d | |||
| 522f136872 | |||
| a28194c601 | |||
| cb2de5505b | |||
| f84c964d10 | |||
| bb4f99f687 | |||
| 49b43820b2 | |||
| b22bbf9639 | |||
| a1ff458007 | |||
| fb263f9d81 | |||
| 72deba43dd | |||
| 24fdb1ca0d | |||
| f20f41c804 | |||
| 66037e2049 | |||
| adb504f2e0 | |||
| 8b90d7827d | |||
| 62e1657274 | |||
| 7f776eebdb | |||
| 0db0513752 | |||
| d8b6b3c306 | |||
| f7c9d732f5 | |||
| 8a95b13711 | |||
| 55884ed1c9 | |||
| 582b25dc44 | |||
| 252af4c863 | |||
| e5ac36facd | |||
| 77fc2d2d4a | |||
| 298adac06e | |||
| 0f920fb867 | |||
| d8e9b50890 | |||
| 02112fbc4f | |||
| 05241649ff | |||
| 4ba00f6216 | |||
| 296cd1b477 | |||
| 2148fe77ec | |||
| 1f692ff01a | |||
| 8e4f943e30 | |||
| a52eba91c9 | |||
| 695d6a1e30 | |||
| a647530d77 | |||
| 18e3f19512 | |||
| 86294c676a | |||
| 7cfd9e5580 | |||
| d29eca457a | |||
| 0855376e02 | |||
| e1692758c6 | |||
| 411167ee44 | |||
| 389efb4701 | |||
| 48f9ac2ba1 | |||
| f9198d4d97 | |||
| eafc3dc0c9 | |||
| f2b176dfda | |||
| 02781927e1 | |||
| 4b3f881e1f | |||
| f3653c1c48 | |||
| 3a1955901e | |||
| e3ce68d2d6 | |||
| ab3d44387e | |||
| 96e24a89e6 | |||
| 25cc6aeb3b | |||
| 4cac92689a | |||
| 7444488278 | |||
| a6cf723dbe | |||
| 7e22b80986 | |||
| 9ec4f3da01 | |||
| ef9ac7a9e3 | |||
| 48426403f3 | |||
| 86c9a17f8d | |||
| d21f2c4d62 | |||
| 733256feab | |||
| f9ce9de316 | |||
| 9f27b17f11 | |||
| 8ff4f1c6e1 | |||
| f6f6259f23 | |||
| c7e9175fd7 | |||
| bdc3c9cb7e | |||
| 790a2b6a4f | |||
| e6c779d91a | |||
| aa95ceafd2 | |||
| 9ef1aa2c60 | |||
| e4107e1257 | |||
| 1d5fef43c1 | |||
| 51280d4e77 | |||
| d5ccc6f777 | |||
| 4ea1bfa9c3 | |||
| c58af2a3af | |||
| d857d54847 | |||
| a7cbed395b | |||
| f6479a0668 | |||
| a50b5c6407 | |||
| 53d19d819c | |||
| a3e337fe2d | |||
| 1bb243bc34 | |||
| faaf3a738a | |||
| e8bbe4bf5a | |||
| 469bba178a | |||
| 879595dbe6 | |||
| f7d2e95d37 | |||
| 9dbc7f5caa | |||
| 1cc35b1d79 | |||
| 2473f210bc | |||
| 604578bb19 | |||
| 1d73aad2ed | |||
| 8508c2dd8e | |||
| a8c5c696be | |||
| 077d460b29 | |||
| f3d9893588 | |||
| c004e1af03 | |||
| 0d227f2bd6 | |||
| 0b96901aee | |||
| d12a5f3b38 | |||
| cd77542a9e | |||
| 0c2660e283 | |||
| 9a591ba29f | |||
| 4c633ee8ff | |||
| 67e9759d35 | |||
| 93dac201b3 | |||
| 946ec70136 | |||
| 161682cf75 | |||
| 6df0093786 | |||
| a527af93b7 | |||
| 775cb2e4ac | |||
| 68b5187f7c | |||
| aacbef3e33 | |||
| f5b2dcfe0a | |||
| 9d25fab5cb | |||
| 250b90634f | |||
| e56716c792 | |||
| 0bf5278168 | |||
| f2c61d2419 | |||
| bdd224990d | |||
| 0ca2ad4930 | |||
| 5140c79dcf | |||
| 91c65b6060 | |||
| afd40a59b5 | |||
| e6177d44c6 | |||
| 63778d2012 | |||
| 7a33cde334 | |||
| c4e29d1c3f | |||
| b6b5deaa58 | |||
| 689a583598 | |||
| 9f6cacdbe1 | |||
| abab461236 | |||
| 23579774b8 | |||
| 0ee85b0ffd | |||
| c99b1a2132 | |||
| 8a9e9495f9 | |||
| 1437f592d2 | |||
| 4ac509f0c8 | |||
| f37440cd3f | |||
| ea83546e42 | |||
| e2b36d735c | |||
| 1ca94080b8 | |||
| 2ad063eb30 | |||
| 2aea2b81c2 | |||
| 3fc81f1baf | |||
| 063d9312c4 | |||
| 04761c9418 | |||
| 620b335dc5 | |||
| a765d738e1 | |||
| 878ec1e782 | |||
| 84d866205b | |||
| a273780472 | |||
| dbea46ba32 | |||
| 50d21828c2 | |||
| a974a5df69 | |||
| 799f6c2d16 | |||
| b8f3182ee3 | |||
| f22bc26355 | |||
| 047cc03002 | |||
| bd49aaffde | |||
| 13f6fd5c66 | |||
| 1fe12d2bfd | |||
| 1cf371fa8b | |||
| ee72a2c3cd | |||
| a5912ba538 | |||
| 19a8d36e07 | |||
| 339aff5d31 | |||
| 4de3f1c7ef | |||
| 6e2030d4d4 | |||
| 665e5e1058 | |||
| 8b9a9c7e2b | |||
| f8f60ef6b0 | |||
| d01a5fc44c | |||
| f713a624f9 | |||
| c789ab2ab7 | |||
| 008a5812f3 | |||
| 06819e172a | |||
| 1112f79cf5 | |||
| 9a54a57cf4 | |||
| c4d620c874 | |||
| 0cc06407ad | |||
| aab065139e | |||
| aa6b5c6b6c | |||
| 9fc3e413a9 | |||
| 289da6bd6d | |||
| 1feb7ec485 | |||
| 5f97a4f8a6 | |||
| 752ebfff3b | |||
| 01d99645a6 | |||
| 00814d51f1 | |||
| e8ff4d8328 | |||
| 8da76e72df | |||
| 652f64cd14 | |||
| b52f0d8dd7 | |||
| f8917e7548 | |||
| 932e9fe9c6 | |||
| 3fd4357503 | |||
| 9b878a9fc0 | |||
| 268d03fad5 | |||
| 3aece81891 | |||
| 6b268a44bb | |||
| 70ce23862b | |||
| 1c32d3a1c9 | |||
| 2814c63765 | |||
| cc3a08e1ab | |||
| 5b3a6d240e | |||
| cc8c4d33ef | |||
| ada31ccf90 | |||
| 4a629be2e2 | |||
| 8897feb290 | |||
| 75f97ea9e1 | |||
| 83b9547701 | |||
| 791f6b1151 | |||
| 1c739e521d | |||
| 794f35fc37 | |||
| 6c93028ce3 | |||
| 49072d43ea | |||
| a5090b8025 | |||
| 36c3cd53e1 | |||
| e4e1c7fb6c | |||
| fcc8de807f | |||
| 9d972f72ea | |||
| 3d629396be | |||
| fe413cb474 | |||
| 277f99e080 | |||
| 584d026256 | |||
| 5acc53311d | |||
| b9ba3a36c4 | |||
| d7c00addd5 | |||
| b34119c4df | |||
| d098291c90 | |||
| ed9e126a4a | |||
| a8010eec15 | |||
| 8cd1ea305c | |||
| 762264b030 | |||
| 48d5369196 | |||
| aab840db2d | |||
| 3526005e1c | |||
| 0ef8c66d54 | |||
| cd9173c802 | |||
| 0c8a185df7 | |||
| 0f0719c596 | |||
| d9397d870f | |||
| 1704a7de7a | |||
| 616cd2e39b | |||
| 222f90bbea | |||
| ab5245d2d9 | |||
| a7a4488848 | |||
| 8e72c1392a | |||
| 70b6af34e1 | |||
| 119462f663 | |||
| 8ee665cd57 | |||
| ace3f181ed | |||
| 6d12737fda | |||
| d3962acd30 | |||
| 18557c8f6e | |||
| 29b037726e | |||
| eae1af331f | |||
| 2e92da500f | |||
| 59dc193f97 | |||
| bd778d259b | |||
| 605cf236a5 | |||
| 8bc3a0d277 | |||
| d80c88bc38 | |||
| dd34a5be57 | |||
| f4be80b724 | |||
| b40c58b78a | |||
| 64468b6f4d | |||
| 8f83b063f1 | |||
| 9eb897b655 | |||
| a738ca0b2c | |||
| f56772a07a | |||
| 38662d2824 | |||
| a51873c06a | |||
| a29543e4a7 | |||
| d4a2ac2796 | |||
| 4b5e09c124 | |||
| f8f48fb98b | |||
| 1b8e7f3643 | |||
| 420ecb320e | |||
| 097f279ecb | |||
| 9a1d4e2b9c | |||
| a926efcd2a | |||
| 40aa319d18 | |||
| 549dabbde6 | |||
| 42e52647f3 | |||
| a12ffd6b5e | |||
| 3d74e77760 | |||
| 0ffd66e747 | |||
| e78fa459cb | |||
| c01b1e9488 | |||
| 45c7b951e9 | |||
| f31f5e7ad1 | |||
| c3a934777d | |||
| 3aba62a6ed | |||
| ad5128f885 | |||
| 376172e5a9 | |||
| 4dd6553e34 | |||
| f2b3a2be30 | |||
| 1e4c932313 | |||
| 7bd9e01a15 | |||
| 69277a287a | |||
| 6c0f17d8f9 | |||
| 6dc9988af0 | |||
| 18656372f8 | |||
| febf492790 | |||
| bcb0c33616 | |||
| 7072933a88 | |||
| f2342bee2d | |||
| 572e9f07ea | |||
| 09d84044b5 | |||
| 9ecf596cbb | |||
| a49a1324a5 | |||
| 8538caeffc | |||
| 79ecd98818 | |||
| f6cad40a44 | |||
| e25368ef7f | |||
| 7ce17a0854 | |||
| c5b5e07a98 | |||
| 2addd06e60 | |||
| 177f2e92f8 | |||
| 217f61902e | |||
| 9274602051 | |||
| efa3b033af | |||
| 18649d3255 | |||
| 2b55d94cce | |||
| 2d17133684 | |||
| e2e6cb0327 | |||
| 5415f1e3cb | |||
| 0f3a79ab54 | |||
| 983e926a23 | |||
| 302aa2b384 | |||
| 84ff9784ca | |||
| 6dfab6d00c | |||
| 628ed9a578 | |||
| 0a6f7b8ebc | |||
| 5b511edca1 | |||
| 4ecbd998bd | |||
| fcd0eb30c6 | |||
| aa4ce3478a | |||
| 1e82781c52 | |||
| edf9dc9279 | |||
| 63c6b6bec5 | |||
| 154a82ab0b | |||
| ab51e97008 | |||
| 0240fc1803 | |||
| d3b862ae91 | |||
| dc2344fd4c | |||
| 0072c554fb | |||
| ed8381c6cb | |||
| 40b4ecb4f8 | |||
| 5268ededbb | |||
| a69e1b940f | |||
| 15994c46d1 | |||
| b565a91194 | |||
| b30937934a | |||
| eec7cd7321 | |||
| f86b84dcc6 | |||
| cd9bca0451 | |||
| ce3043b595 | |||
| 68c5ebb000 | |||
| 08e91e921b | |||
| 6c2ca794f5 | |||
| b3fd408cf7 | |||
| dc96d126bb | |||
| 07fd622901 | |||
| bb74cbb56b | |||
| ffab60b4b8 | |||
| 2309fc53fb | |||
| e153ffee4e | |||
| a3d741d685 | |||
| a12e35c5f0 | |||
| 5f3e664eed | |||
| fb69afd5c4 | |||
| 8c9eeaa133 | |||
| 00f59e9555 | |||
| 767c45080f | |||
| b0758e02aa | |||
| 7f81836d9f | |||
| 33c3ef6751 | |||
| 20056f209e | |||
| 69d848f22b | |||
| 1557a44934 | |||
| 89dd63a0ee | |||
| e5c55145e9 | |||
| 2638d62a24 | |||
| e04d839640 | |||
| 0158e2fd5d | |||
| 3e308f2082 | |||
| 429224993b | |||
| 1b8271d217 | |||
| 29ef6b5b43 | |||
| 23a42659b3 | |||
| 9d7a5eccd3 | |||
| 42bdcf54ca | |||
| 251b65d04d | |||
| b72da01632 | |||
| e2f337f882 | |||
| 496c0178de | |||
| ef94de48c2 | |||
| 14a8b0ee4e | |||
| 38d31ae737 | |||
| d3beafec74 | |||
| 73fb5bb3c6 | |||
| 247a3b7feb | |||
| 2e5be73f0c | |||
| 2ca1db35af | |||
| 11baebdf54 | |||
| 8dde88222c | |||
| 2b47e9745f | |||
| f96416be38 | |||
| a4ef0de0ac | |||
| c3e2dc1621 | |||
| f7a905e27f | |||
| c579bcc2fd | |||
| ad7b7a6866 | |||
| bed7929e48 | |||
| b07842ec3f | |||
| 19b2b8bd77 | |||
| f4a407d80f | |||
| 95be6e0b37 | |||
| 6741a6fceb | |||
| db560744fb | |||
| 8937b52e72 | |||
| 7550b96389 | |||
| 8aa3282c3d | |||
| 9ed68d5fac | |||
| 7668d3dee7 | |||
| fc65d847c4 | |||
| 653d5e9c1f | |||
| 631f724138 | |||
| 162bf704cc | |||
| d64e9c93f5 | |||
| 74c610632e | |||
| 2496a2c42e | |||
| 656b24b701 | |||
| 3fd9b572fe | |||
| dab75a6614 | |||
| 4747691e74 | |||
| 6dac9626c0 | |||
| 180df75bb4 | |||
| b8a5e5b67c | |||
| 5039ca8fdf | |||
| aea35c95f2 | |||
| 0a5ebc1c50 | |||
| cbe3d4bd45 | |||
| 16b84ec933 | |||
| 2661f5e8e1 | |||
| 760f8c447c | |||
| 57db928f7a | |||
| 9b130edcfe | |||
| ff0dcd5095 | |||
| 30b528402c | |||
| 6d513a1e60 | |||
| 7269c4e582 | |||
| 083bbf7d9f | |||
| e54f49ebfb | |||
| 31cd10b9e6 | |||
| c6dcb8eb9e | |||
| 3a98d1edb1 | |||
| 22d32aca3a | |||
| fe5e02c3a9 | |||
| 3d58627f30 | |||
| 20fd8f318b | |||
| 98ee94018e | |||
| ac9bc80e20 | |||
| 8203b42169 | |||
| bc027b2fca | |||
| afa24231e6 | |||
| da5bb4b68e | |||
| f2d741d01f | |||
| e893dc0327 | |||
| e05a399177 | |||
| 4f285ce364 | |||
| 2be5a2d13a | |||
| 5e5fe80901 | |||
| 44bc8bfdbf | |||
| 4f9b5ac873 | |||
| 1beb011bfa | |||
| bf9029f9bc | |||
| d7a3c44e31 | |||
| 7b31f2841a | |||
| e8cf6ee9e7 | |||
| 2b3f828654 | |||
| 7ee1840e77 | |||
| 1843cd0878 | |||
| 78e10a5593 | |||
| 7bcefd4536 | |||
| 6d5f885306 | |||
| 99adb23149 | |||
| 043a38bd36 | |||
| 0269411c16 | |||
| f4ceb53738 |
@@ -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.
|
||||
|
||||
|
||||
@@ -10,6 +10,7 @@
|
||||
"jsdoc",
|
||||
"header",
|
||||
"local"
|
||||
// "react" // Void
|
||||
],
|
||||
"rules": {
|
||||
"constructor-super": "warn",
|
||||
|
||||
@@ -21,3 +21,4 @@ vscode.db
|
||||
product.overrides.json
|
||||
*.snap.actual
|
||||
.vscode-test
|
||||
.tmp
|
||||
|
||||
@@ -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";
|
||||
};
|
||||
};
|
||||
};
|
||||
};
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
## Jan. 13, 2025 - Entering beta
|
||||
|
||||
- Added quick edits! Void handles FIM-prompting, output parsing, and history management for inline UI.
|
||||
|
||||
- Migrated away from VS Code extension API - Void now lives and interacts entirely within the VS Code codebase.
|
||||
|
||||
- New settings page with model configuration, one-click switch, and user settings.
|
||||
|
||||
- Added auto-detection (via polling) of local models by default.
|
||||
|
||||
- LLM requests originate from `node/`, which fixes common CORS and CSP issues when running some models locally.
|
||||
|
||||
- Misc improvements like UI and history for Accept | Reject in the sidebar and editor, streaming interruptions, and past chat history.
|
||||
|
||||
- Automatic file selection on tab switches.
|
||||
|
||||
- Lots of new UI, misc bug fixes, and performance improvements.
|
||||
|
||||
- VS Code's default Ctrl+L is now Ctrl+M in Void (on Mac Cmd+L becomes Cmd+M).
|
||||
|
||||
- Switched from the MIT License to the Apache 2.0 License. Apache's attribution clause provides a small amount of protection to our source initiative.
|
||||
|
||||
A huge shoutout to our many contributors. If you'd like to help build Void,
|
||||
|
||||
## Sept/Oct. 2024 - Early launch
|
||||
|
||||
- Initialized Void's website and GitHub repo.
|
||||
|
||||
- Started a waitlist.
|
||||
|
||||
@@ -1,150 +1,153 @@
|
||||
# Contributing to Void
|
||||
|
||||
Welcome! 👋 This is a guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
|
||||
Welcome! 👋 This is the official guide on how to contribute to Void. We want to make it as easy as possible to contribute, so if you have any questions or comments, reach out via email or discord!
|
||||
|
||||
There are two main ways to contribute:
|
||||
There are a few ways to contribute:
|
||||
|
||||
- Suggest New Features ([discord](https://discord.gg/4GAxHVAD))
|
||||
- Build New Features ([project](https://github.com/orgs/voideditor/projects/2/views/3))
|
||||
- 👨💻 Build new features - see [Issues](https://github.com/voideditor/void/issues).
|
||||
- 💡 Make suggestions in our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
- ⭐️ If you want to build your AI tool into Void, feel free to get in touch! It's very easy to extend Void, and the UX you create will be much more natural than a VSCode Extension.
|
||||
|
||||
We use a [VSCode extension](https://code.visualstudio.com/api/get-started/your-first-extension) to implement most of Void's functionality. Scroll down to see 1. How to build/contribute to the Extension, or 2. How to build/contribute to the full IDE (for more native changes).
|
||||
Void's code lives in `src/vs/workbench/contrib/void/browser/` and `src/vs/platform/void/`.
|
||||
|
||||
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
## 1. Building the Extension
|
||||
Here's how you can start contributing to the Void extension. This is where you should get started if you're new.
|
||||
|
||||
1. Clone the repository:
|
||||
## Building the full IDE
|
||||
|
||||
### a. Build Prerequisites - Mac
|
||||
|
||||
If you're using a Mac, you need Python and XCode. You probably have these by default.
|
||||
|
||||
### b. Build Prerequisites - Windows
|
||||
|
||||
If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them.
|
||||
|
||||
Go to the "Workloads" tab and select:
|
||||
- `Desktop development with C++`
|
||||
- `Node.js build tools`
|
||||
|
||||
Go to the "Individual Components" tab and select:
|
||||
- `MSVC v143 - VS 2022 C++ x64/x86 Spectre-mitigated libs (Latest)`
|
||||
- `C++ ATL for latest build tools with Spectre Mitigations`
|
||||
- `C++ MFC for latest build tools with Spectre Mitigations`
|
||||
|
||||
Finally, click Install.
|
||||
|
||||
### c. Build Prerequisites - Linux
|
||||
|
||||
First, run `npm install -g node-gyp`. Then:
|
||||
|
||||
- Debian (Ubuntu, etc): `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
|
||||
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
|
||||
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
|
||||
|
||||
### Building Void
|
||||
|
||||
To build Void, open `void/` inside VSCode. Then open your terminal and run:
|
||||
|
||||
1. `npm install` to install all dependencies.
|
||||
2. `npm run watchreact` to build Void's browser dependencies like React. (If this doesn't work, try `npm run buildreact`).
|
||||
3. Build Void.
|
||||
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
|
||||
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
|
||||
- This step can take ~5 min. The build is done when you see two check marks.
|
||||
4. Run Void.
|
||||
- Run `./scripts/code.sh` (Mac/Linux).
|
||||
- Run `./scripts/code.bat` (Windows).
|
||||
- This command should open up the built IDE. You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to see changes without re-building, or press or <kbd>Ctrl+Shift+P</kbd> in the new window and run "Reload Window".
|
||||
- If you are actively developing Void, we strongly recommend adding the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command (just append them at the end of the string). This will save all data and extensions to the `.tmp` folder. You can delete this folder to reset any IDE changes you made when testing.
|
||||
|
||||
#### Building Void from Terminal
|
||||
|
||||
Alternatively, if you want to build Void from the terminal, instead of pressing <kbd>Cmd+Shift+B</kbd> you can run `npm run watch`. The build is done when you see something like this:
|
||||
|
||||
```
|
||||
git clone https://github.com/voideditor/void
|
||||
[watch-extensions] [00:37:39] Finished compilation extensions with 0 errors after 19303 ms
|
||||
[watch-client ] [00:38:06] Finished compilation with 0 errors after 46248 ms
|
||||
[watch-client ] [00:38:07] Starting compilation...
|
||||
[watch-client ] [00:38:07] Finished compilation with 0 errors after 5 ms
|
||||
```
|
||||
|
||||
2. Open the folder `/extensions/void` in VS Code (open it in a new workspace, *don't* just cd into it):
|
||||
|
||||
```
|
||||
open /extensions/void
|
||||
|
||||
### Common Fixes
|
||||
|
||||
- Make sure you follow the prerequisite steps.
|
||||
- Make sure you have the same NodeJS version as `.nvmrc`.
|
||||
- Make sure your `npm run watchreact` is running if you change any React files, or else you'll need to re-build.
|
||||
- If you see missing styles, go to `src2/styles.css` and re-save the file.
|
||||
- If you get `"TypeError: Failed to fetch dynamically imported module: vscode-file://vscode-app/.../workbench.desktop.main.js", source: file:///.../bootstrap-window.js`, make sure all imports end with `.js`.
|
||||
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). For building questions, you can also refer to VSCode's full [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
|
||||
|
||||
## Bundling
|
||||
|
||||
We don't usually recommend bundling. Instead, you should probably just build. If you're sure you want to bundle Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - compiling can take ~25 minutes.
|
||||
|
||||
|
||||
### Mac
|
||||
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
|
||||
- `npm run gulp vscode-darwin-x64` (Intel)
|
||||
|
||||
### Windows
|
||||
- `npm run gulp vscode-win32-x64` - most common
|
||||
- `npm run gulp vscode-win32-ia32`
|
||||
|
||||
### Linux
|
||||
- `npm run gulp vscode-linux-x64` - most common
|
||||
- `npm run gulp vscode-linux-arm`
|
||||
- `npm run gulp vscode-linux-ia32`
|
||||
|
||||
|
||||
### Output
|
||||
|
||||
This will generate a folder outside of `void/`:
|
||||
```bash
|
||||
workspace/
|
||||
├── void/ # Your Void fork
|
||||
└── VSCode-darwin-arm64/ # Generated output
|
||||
```
|
||||
|
||||
3. Install dependencies:
|
||||
|
||||
```
|
||||
npm install
|
||||
```
|
||||
|
||||
4. Build the project. We created this build command so that we could run React in vscode - it converts `sidebar/index.tsx` into a CSS/JS bundle in `dist/`.
|
||||
|
||||
```
|
||||
npm run build
|
||||
```
|
||||
|
||||
5. Run the project by pressing <kbd>F5</kbd>.
|
||||
|
||||
This will start a new instance of VS Code with the extension enabled. If this does not work, you can press <kbd>Ctrl+Shift+P</kbd>, select "Debug: Start Debugging", and select "VS Code Extension Development".
|
||||
|
||||
If you would like to use AI features, you need to provide an API key. You can do that by going to Settings (Ctrl+,) typing in "void", and adding the API key you want to use (eg. the `"Anthropic Api Key"` environment variable). The "Which API" environment variable controls the provider and defaults to "anthropic".
|
||||
|
||||
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
|
||||
|
||||
## 2. Building the full IDE
|
||||
|
||||
Beyond the extension, we very occasionally edit the IDE when we need to access more functionality. If you want to work on the full IDE, please follow the steps below, or see VS Code's full [how to contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
|
||||
|
||||
Before starting, make sure you've built the extension (by running `cd .\extensions\void\` and `npm run build`).
|
||||
|
||||
Make sure you're on the correct NodeJS version as per `.nvmrc`.
|
||||
|
||||
1. Install all dependencies.
|
||||
|
||||
```
|
||||
yarn
|
||||
```
|
||||
|
||||
2. In VS Code, press <kbd>Ctrl+Shift+B</kbd> to start the build process - this can take some time. If you're not using VS Code, run `npm run watch` instead.
|
||||
|
||||
3. Run `./scripts/code.sh` in your terminal.
|
||||
|
||||
This should open up the built IDE after loading for some time. To see new changes without restarting the build, use <kbd>Ctrl+Shift+P</kbd> and run "Reload Window".
|
||||
|
||||
To bundle the IDE, run `npm run gulp vscode-win32-x64`. Here are the full options: vscode-{win32-ia32 | win32-x64 | darwin-x64 | darwin-arm64 | linux-ia32 | linux-x64 | linux-arm}(-min)
|
||||
|
||||
If you're on Windows, we recommend running the project inside a dev container. VSCode should prompt you to do this automatically.
|
||||
|
||||
Now that you're set up, feel free to check out our [Issues](https://github.com/voideditor/void/issues) page!
|
||||
|
||||
## Roadmap
|
||||
|
||||
Here are the most important topics on our Roadmap. More ⭐'s = more important.
|
||||
|
||||
## ⭐⭐⭐ Improve diffs.
|
||||
|
||||
We define a "diff" as a single green/red pair that denotes a change. Here are improvements to make:
|
||||
|
||||
1. Show deletion (-) diffs. Right now we're only showing insertion (+) diffs. Diffs currently work by highlighting all of the new code in green with a simple text decoration. Instead, we would like to use code from VS Code's native diffEditor to show the diffs ("inline" mode). We could alternatively keep what we have and add red zones of the deleted code to indicate a deletion diff (-).
|
||||
|
||||
2. Fix bugginess when the user presses "Accept" or "Reject" on a diff. One issue is that when a diff is accepted/rejected all of the diffs below should be updated (because they are now on different line numbers). There are other miscellaneous issues too.
|
||||
|
||||
3. Make diff highlighting dynamic. Right now when the user edits text, all of the diffs and their highlights are cleared. Instead, we should update the highlighting of the diff. Each diff lives on a range of lines, and all changes inside that range or intersecting with it should update its highlighting.
|
||||
|
||||
## ⭐⭐⭐ Build Cursor-style quick edits (ctrl+k).
|
||||
|
||||
When the user presses ctrl+k, an input box should appear inline with the code that they were selecting. This is somewhat difficult to do because an extension alone cannot do this, and it requires creating a new component in the IDE. We think you can modify vscode's built-in "codelens" or "zone widget" components, but we are open to alternatives.
|
||||
|
||||
## ⭐⭐⭐ Make History work well.
|
||||
|
||||
When the user submits a response or presses the apply/accept/reject button, we should add these events to the history, allowing the user to undo/redo them. Right now there is unexpected behavior if the user tries to undo or redo their changes.
|
||||
|
||||
## ⭐⭐⭐ Improve Ctrl+L backend.
|
||||
|
||||
Right now, the model outputs entire files. Instead, we should change the prompt so that the model outputs partial changes like `// ... rest of file`. When the user clicks the "Apply" button, the model should rewrite the file and apply the partial changes in the correct locations.
|
||||
|
||||
## ⭐⭐ Integrate with Ollama.
|
||||
|
||||
We have an Ollama integration coded up in the extension, but it breaks. This is because Ollama has Node.js dependencies like 'path' and 'os' which cannot run in extensions (extensions have to be able to run in the browser). To fix this, we need to migrate Void's extension so that it runs natively into the VS Code editor so that we can access Node.js.
|
||||
|
||||
## ⭐⭐⭐ Creative.
|
||||
|
||||
Feel free to build AI features beyond the standard Cursor ones. For example, creating better code search, or supporting AI agents that can edit across files and make multiple LLM calls.
|
||||
|
||||
Eventually, we want to build a convenient API for creating AI tools. The API will provide methods for creating the UI (showing an autocomplete suggestion, or creating a new diff), detecting event changes (like `onKeystroke` or `onFileOpen`), and modifying the user's file-system (storing indexes associated with each file), making it much easier to make your own AI plugin. We plan on building these features further along in timeline, but we wanted to list them for completeness.
|
||||
|
||||
## ⭐ One-stars.
|
||||
|
||||
⭐ When user presses ctrl+L it should clear the sidebar's state.
|
||||
|
||||
⭐ Let the user accept / reject all Diffs in an entire file via the sidebar.
|
||||
|
||||
⭐ Allow the user to make multiple selections of code or files at once.
|
||||
|
||||
⭐ Allow user to X out of their current selection.
|
||||
|
||||
# Guidelines
|
||||
|
||||
Please don't make big refactors without speaking with us first. We'd like to keep the codebase similar to vscode so we can periodically rebase, and if we have big changes that gets complicated.
|
||||
|
||||
# Submitting a Pull Request
|
||||
|
||||
When you've made changes and want to submit them, please submit a pull request.
|
||||
We're always glad to talk about new ideas, help you get set up, and make sure your changes align with our vision for the project! Feel free to shoot Mat or Andrew a message, or start chatting with us in the `#contributing` channel of our [Discord](https://discord.gg/RSNjgaugJs).
|
||||
|
||||
|
||||
## Submitting a Pull Request
|
||||
|
||||
- Please submit a pull request once you've made a change. No need to submit an Issue unless you're creating a new feature.
|
||||
- Please don't use AI to write your PR 🙂.
|
||||
|
||||
|
||||
<!--
|
||||
# Relevant files
|
||||
|
||||
We keep track of all the files we've changed with Void so it's easy to rebase:
|
||||
|
||||
|
||||
- README.md
|
||||
- CONTRIBUTING.md
|
||||
- VOID_USEFUL_LINKS.md
|
||||
- product.json
|
||||
- package.json
|
||||
|
||||
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
|
||||
- src/vs/workbench/workbench.common.main.ts
|
||||
- src/vs/workbench/contrib/void
|
||||
- extensions/void
|
||||
- src/vs/workbench/contrib/void/\*
|
||||
- extensions/void/\*
|
||||
|
||||
- .github/
|
||||
- .vscode/settings
|
||||
- .github/\*
|
||||
- .vscode/settings/\*
|
||||
- .eslintrc.json
|
||||
- build/hygiene.js
|
||||
- build/lib/i18n.resources.json
|
||||
- build/npm/dirs.js
|
||||
|
||||
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
|
||||
-->
|
||||
|
||||
## References
|
||||
|
||||
For some useful links we've compiled on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
@@ -0,0 +1,26 @@
|
||||
Void is a fork of VS Code, which is licensed under the MIT License (below).
|
||||
Void's additions and modifications are licensed under the Apache 2.0 License (see LICENSE.txt).
|
||||
|
||||
--------------------
|
||||
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -1,21 +1,201 @@
|
||||
MIT License
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
Copyright (c) 2015 - present Microsoft Corporation
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
1. Definitions.
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2025 Glass Devtools, Inc.
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
|
||||
@@ -1,22 +1,39 @@
|
||||
# Welcome to Void.
|
||||
|
||||
<div align="center">
|
||||
<img
|
||||
src="./src/vs/workbench/browser/parts/editor/media/slice_of_void.png"
|
||||
alt="Void Welcome"
|
||||
width="300"
|
||||
height="300"
|
||||
/>
|
||||
</div>
|
||||
|
||||
Void is the open-source Cursor alternative.
|
||||
Void is the open-source Cursor alternative. This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/) for downloading the official release. If you're new, welcome!
|
||||
|
||||
If you're new, welcome! Feel free to check out our [Project board](https://github.com/orgs/voideditor/projects/2/views/3) for the most pressing Issues to work on, and see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md) for instructions on building and running Void.
|
||||
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
|
||||
|
||||
This repo contains the full sourcecode for Void. We have a [waitlist](https://voideditor.com/email) for downloading the official release, but you can build and develop Void right now.
|
||||
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
|
||||
|
||||
- 📝 [Changelog](https://voideditor.com/changelog)
|
||||
|
||||
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md)
|
||||
|
||||
|
||||
<!-- ❤️ Setup: -->
|
||||
|
||||
## Contributing
|
||||
|
||||
To build and run Void, see [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
1. To contribute to the Void repository, follow the steps in [`CONTRIBUTING.md`](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
|
||||
|
||||
2. To suggest a new feature, please create a new [GitHub Issue](https://github.com/voideditor/void/issues). Suggestions will be added to our Roadmap within one day.
|
||||
|
||||
3. We're open to collaborations of all types. Feel free to contact us via email or discord.
|
||||
|
||||
## Reference
|
||||
|
||||
Void is a fork of the of [vscode](https://github.com/microsoft/vscode) repository.
|
||||
|
||||
For some useful links we've compiled see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For some useful links on VSCode, see [`VOID_USEFUL_LINKS.md`](https://github.com/voideditor/void/blob/main/VOID_USEFUL_LINKS.md).
|
||||
|
||||
## Support
|
||||
Feel free to reach out in our [Discord](https://discord.gg/PspNkKG5wt) or contact us via email.
|
||||
Feel free to reach out in our discord or contact us via email.
|
||||
|
||||
@@ -1,41 +0,0 @@
|
||||
<!-- BEGIN MICROSOFT SECURITY.MD V0.0.9 BLOCK -->
|
||||
|
||||
## Security
|
||||
|
||||
Microsoft takes the security of our software products and services seriously, which includes all source code repositories managed through our GitHub organizations, which include [Microsoft](https://github.com/Microsoft), [Azure](https://github.com/Azure), [DotNet](https://github.com/dotnet), [AspNet](https://github.com/aspnet) and [Xamarin](https://github.com/xamarin).
|
||||
|
||||
If you believe you have found a security vulnerability in any Microsoft-owned repository that meets [Microsoft's definition of a security vulnerability](https://aka.ms/security.md/definition), please report it to us as described below.
|
||||
|
||||
## Reporting Security Issues
|
||||
|
||||
**Please do not report security vulnerabilities through public GitHub issues.**
|
||||
|
||||
Instead, please report them to the Microsoft Security Response Center (MSRC) at [https://msrc.microsoft.com/create-report](https://aka.ms/security.md/msrc/create-report).
|
||||
|
||||
If you prefer to submit without logging in, send email to [secure@microsoft.com](mailto:secure@microsoft.com). If possible, encrypt your message with our PGP key; please download it from the [Microsoft Security Response Center PGP Key page](https://aka.ms/security.md/msrc/pgp).
|
||||
|
||||
You should receive a response within 24 hours. If for some reason you do not, please follow up via email to ensure we received your original message. Additional information can be found at [microsoft.com/msrc](https://www.microsoft.com/msrc).
|
||||
|
||||
Please include the requested information listed below (as much as you can provide) to help us better understand the nature and scope of the possible issue:
|
||||
|
||||
* Type of issue (e.g. buffer overflow, SQL injection, cross-site scripting, etc.)
|
||||
* Full paths of source file(s) related to the manifestation of the issue
|
||||
* The location of the affected source code (tag/branch/commit or direct URL)
|
||||
* Any special configuration required to reproduce the issue
|
||||
* Step-by-step instructions to reproduce the issue
|
||||
* Proof-of-concept or exploit code (if possible)
|
||||
* Impact of the issue, including how an attacker might exploit the issue
|
||||
|
||||
This information will help us triage your report more quickly.
|
||||
|
||||
If you are reporting for a bug bounty, more complete reports can contribute to a higher bounty award. Please visit our [Microsoft Bug Bounty Program](https://aka.ms/security.md/msrc/bounty) page for more details about our active programs.
|
||||
|
||||
## Preferred Languages
|
||||
|
||||
We prefer all communications to be in English.
|
||||
|
||||
## Policy
|
||||
|
||||
Microsoft follows the principle of [Coordinated Vulnerability Disclosure](https://aka.ms/security.md/cvd).
|
||||
|
||||
<!-- END MICROSOFT SECURITY.MD BLOCK -->
|
||||
@@ -2,23 +2,27 @@
|
||||
|
||||
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
|
||||
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
|
||||
|
||||
## Beginners / Getting started
|
||||
|
||||
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
|
||||
|
||||
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
|
||||
|
||||
## Contributing
|
||||
|
||||
- [How VS Code's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
|
||||
## Misc
|
||||
|
||||
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - sometimes useful to reference.
|
||||
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
|
||||
|
||||
|
||||
## VSCode's Extension API
|
||||
|
||||
Void is mainly an extension right now, and these links were very useful for us to get set up.
|
||||
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
|
||||
|
||||
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
|
||||
|
||||
@@ -28,6 +32,6 @@ Void is mainly an extension right now, and these links were very useful for us t
|
||||
|
||||
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
|
||||
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful)
|
||||
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
|
||||
|
||||
|
||||
|
||||
@@ -1,3 +1,5 @@
|
||||
# Void - this looks like the relevant file for us (product-build-darwin.yml is independent and maybe just used for testing)
|
||||
|
||||
steps:
|
||||
- task: NodeTool@0
|
||||
inputs:
|
||||
@@ -59,6 +61,8 @@ steps:
|
||||
- script: node build/azure-pipelines/distro/mixin-quality
|
||||
displayName: Mixin distro quality
|
||||
|
||||
|
||||
## Void - IMPORTANT
|
||||
- script: |
|
||||
set -e
|
||||
unzip $(Pipeline.Workspace)/unsigned_vscode_client_darwin_x64_archive/VSCode-darwin-x64.zip -d $(agent.builddirectory)/VSCode-darwin-x64
|
||||
@@ -66,6 +70,7 @@ steps:
|
||||
DEBUG=* node build/darwin/create-universal-app.js $(agent.builddirectory)
|
||||
displayName: Create Universal App
|
||||
|
||||
## Void - IMPORTANT
|
||||
- script: |
|
||||
set -e
|
||||
security create-keychain -p pwd $(agent.tempdirectory)/buildagent.keychain
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
Object.defineProperty(exports, "__esModule", { value: true });
|
||||
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
|
||||
const path = require("path");
|
||||
const fs = require("fs");
|
||||
const minimatch = require("minimatch");
|
||||
|
||||
@@ -3,6 +3,8 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
// Void explanation - product-build-darwin-universal.yml runs this (create-universal-app.ts), then sign.ts
|
||||
|
||||
import * as path from 'path';
|
||||
import * as fs from 'fs';
|
||||
import * as minimatch from 'minimatch';
|
||||
|
||||
@@ -78,24 +78,24 @@ async function main(buildDir) {
|
||||
// universal will get its copy from the x64 build.
|
||||
if (arch !== 'universal') {
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-insert',
|
||||
'-replace', // Void changed this to replace
|
||||
'NSAppleEventsUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use AppleScript.',
|
||||
'An application in Void wants to use AppleScript.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-replace',
|
||||
'NSMicrophoneUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Microphone.',
|
||||
'An application in Void wants to use the Microphone.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await (0, cross_spawn_promise_1.spawn)('plutil', [
|
||||
'-replace',
|
||||
'NSCameraUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Camera.',
|
||||
'An application in Void wants to use the Camera.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -89,24 +89,24 @@ async function main(buildDir?: string): Promise<void> {
|
||||
// universal will get its copy from the x64 build.
|
||||
if (arch !== 'universal') {
|
||||
await spawn('plutil', [
|
||||
'-insert',
|
||||
'-replace', // Void changed this to replace
|
||||
'NSAppleEventsUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use AppleScript.',
|
||||
'An application in Void wants to use AppleScript.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await spawn('plutil', [
|
||||
'-replace',
|
||||
'NSMicrophoneUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Microphone.',
|
||||
'An application in Void wants to use the Microphone.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
await spawn('plutil', [
|
||||
'-replace',
|
||||
'NSCameraUsageDescription',
|
||||
'-string',
|
||||
'An application in Visual Studio Code wants to use the Camera.',
|
||||
'An application in Void wants to use the Camera.',
|
||||
`${infoPlistPath}`
|
||||
]);
|
||||
}
|
||||
|
||||
@@ -341,6 +341,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
this.emit('data', file);
|
||||
}));
|
||||
|
||||
|
||||
// Void - this is important, creates the product.json in .app
|
||||
let productJsonContents;
|
||||
const productJsonStream = gulp.src(['product.json'], { base: '.' })
|
||||
.pipe(json({ commit, date: readISODate('out-build'), checksums, version }))
|
||||
@@ -425,15 +427,15 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
|
||||
'resources/win32/vue.ico',
|
||||
'resources/win32/xml.ico',
|
||||
'resources/win32/yaml.ico',
|
||||
'resources/win32/code_70x70.png',
|
||||
'resources/win32/code_150x150.png'
|
||||
'resources/win32/code_70x70.png', // <-- Void icon
|
||||
'resources/win32/code_150x150.png' // <-- Void icon
|
||||
], { base: '.' }));
|
||||
} else if (platform === 'linux') {
|
||||
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' }));
|
||||
all = es.merge(all, gulp.src('resources/linux/code.png', { base: '.' })); // <-- Void icon
|
||||
} else if (platform === 'darwin') {
|
||||
const shortcut = gulp.src('resources/darwin/bin/code.sh')
|
||||
.pipe(replace('@@APPNAME@@', product.applicationName))
|
||||
.pipe(rename('bin/code'));
|
||||
.pipe(rename('bin/code')); // <-- Void icon
|
||||
|
||||
all = es.merge(all, shortcut);
|
||||
}
|
||||
|
||||
@@ -54,7 +54,7 @@ function darwinBundleDocumentType(extensions, icon, nameOrSuffix, utis) {
|
||||
role: 'Editor',
|
||||
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
|
||||
extensions,
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
|
||||
utis
|
||||
};
|
||||
}
|
||||
@@ -179,7 +179,7 @@ exports.config = {
|
||||
darwinForceDarkModeSupport: true,
|
||||
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
|
||||
linuxExecutableName: product.applicationName,
|
||||
winIcon: 'resources/win32/code.ico',
|
||||
winIcon: 'resources/win32/code.ico', // <-- Void icon
|
||||
token: process.env['GITHUB_TOKEN'],
|
||||
repo: product.electronRepository || undefined,
|
||||
validateChecksum: true,
|
||||
|
||||
@@ -68,7 +68,7 @@ function darwinBundleDocumentType(extensions: string[], icon: string, nameOrSuff
|
||||
role: 'Editor',
|
||||
ostypes: ['TEXT', 'utxt', 'TUTX', '****'],
|
||||
extensions,
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns',
|
||||
iconFile: 'resources/darwin/' + icon.toLowerCase() + '.icns', // <-- Void icon code.icns
|
||||
utis
|
||||
};
|
||||
}
|
||||
@@ -196,7 +196,7 @@ export const config = {
|
||||
darwinForceDarkModeSupport: true,
|
||||
darwinCredits: darwinCreditsTemplate ? Buffer.from(darwinCreditsTemplate({ commit: commit, date: new Date().toISOString() })) : undefined,
|
||||
linuxExecutableName: product.applicationName,
|
||||
winIcon: 'resources/win32/code.ico',
|
||||
winIcon: 'resources/win32/code.ico', // <-- Void icon
|
||||
token: process.env['GITHUB_TOKEN'],
|
||||
repo: product.electronRepository || undefined,
|
||||
validateChecksum: true,
|
||||
|
||||
@@ -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`)) {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -4,6 +4,7 @@
|
||||
? ('; LicenseFile: "' + RepoDir + '\licenses\LICENSE-' + Language + '.rtf"') \
|
||||
: '; LicenseFile: "' + RepoDir + '\' + RootLicenseFileName + '"'
|
||||
|
||||
|
||||
[Setup]
|
||||
AppId={#AppId}
|
||||
AppName={#NameLong}
|
||||
@@ -20,8 +21,10 @@ Compression=lzma
|
||||
SolidCompression=yes
|
||||
AppMutex={code:GetAppMutex}
|
||||
SetupMutex={#AppMutex}setup
|
||||
WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp"
|
||||
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp"
|
||||
; this is a Void icon comment. Old: WizardImageFile="{#RepoDir}\resources\win32\inno-big-100.bmp,{#RepoDir}\resources\win32\inno-big-125.bmp,{#RepoDir}\resources\win32\inno-big-150.bmp,{#RepoDir}\resources\win32\inno-big-175.bmp,{#RepoDir}\resources\win32\inno-big-200.bmp,{#RepoDir}\resources\win32\inno-big-225.bmp,{#RepoDir}\resources\win32\inno-big-250.bmp"
|
||||
; this is a Void icon comment. Old: WizardSmallImageFile="{#RepoDir}\resources\win32\inno-small-100.bmp,{#RepoDir}\resources\win32\inno-small-125.bmp,{#RepoDir}\resources\win32\inno-small-150.bmp,{#RepoDir}\resources\win32\inno-small-175.bmp,{#RepoDir}\resources\win32\inno-small-200.bmp,{#RepoDir}\resources\win32\inno-small-225.bmp,{#RepoDir}\resources\win32\inno-small-250.bmp"
|
||||
; WizardImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
WizardSmallImageFile="{#RepoDir}\resources\win32\inno-void.bmp"
|
||||
SetupIconFile={#RepoDir}\resources\win32\code.ico
|
||||
UninstallDisplayIcon={app}\{#ExeBasename}.exe
|
||||
ChangesEnvironment=true
|
||||
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
@@ -1,5 +0,0 @@
|
||||
out
|
||||
dist
|
||||
node_modules
|
||||
.vscode-test/
|
||||
*.vsix
|
||||
@@ -1,5 +0,0 @@
|
||||
import { defineConfig } from '@vscode/test-cli';
|
||||
|
||||
export default defineConfig({
|
||||
files: 'out/test/**/*.test.js',
|
||||
});
|
||||
@@ -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"
|
||||
]
|
||||
}
|
||||
@@ -1,21 +0,0 @@
|
||||
// A launch configuration that compiles the extension and then opens it inside a new window
|
||||
// Use IntelliSense to learn about possible attributes.
|
||||
// Hover to view descriptions of existing attributes.
|
||||
// For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387
|
||||
{
|
||||
"version": "0.2.0",
|
||||
"configurations": [
|
||||
{
|
||||
"name": "Run Extension",
|
||||
"type": "extensionHost",
|
||||
"request": "launch",
|
||||
"args": [
|
||||
"--extensionDevelopmentPath=${workspaceFolder}"
|
||||
],
|
||||
"outFiles": [
|
||||
"${workspaceFolder}/out/**/*.js"
|
||||
],
|
||||
"preLaunchTask": "${defaultBuildTask}"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -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": false
|
||||
},
|
||||
"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",
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -1,11 +0,0 @@
|
||||
.vscode/**
|
||||
.vscode-test/**
|
||||
src/**
|
||||
.gitignore
|
||||
.yarnrc
|
||||
vsc-extension-quickstart.md
|
||||
**/tsconfig.json
|
||||
**/.eslintrc.json
|
||||
**/*.map
|
||||
**/*.ts
|
||||
**/.vscode-test.*
|
||||
@@ -1 +0,0 @@
|
||||
Please see the `CONTRIBUTING.md` for information on how to contribute :)!
|
||||
@@ -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").
|
||||
@@ -1,19 +0,0 @@
|
||||
const tailwindcss = require('tailwindcss')
|
||||
const autoprefixer = require('autoprefixer')
|
||||
const postcss = require('postcss')
|
||||
const fs = require('fs')
|
||||
|
||||
const from = 'src/sidebar/styles.css'
|
||||
const to = 'dist/sidebar/styles.css'
|
||||
|
||||
const original_css_contents = fs.readFileSync(from, 'utf8')
|
||||
|
||||
postcss([
|
||||
tailwindcss, // this compiles tailwind of all the files specified in tailwind.config.json
|
||||
autoprefixer,
|
||||
])
|
||||
.process(original_css_contents, { from, to })
|
||||
.then(processed_css_contents => { fs.writeFileSync(to, processed_css_contents.css) })
|
||||
.catch(error => {
|
||||
console.error('Error in build-css:', error)
|
||||
})
|
||||
@@ -1,13 +0,0 @@
|
||||
const esbuild = require('esbuild')
|
||||
|
||||
// Build JS
|
||||
esbuild.build({
|
||||
entryPoints: ['src/sidebar/index.tsx'],
|
||||
bundle: true,
|
||||
minify: true,
|
||||
sourcemap: true,
|
||||
outfile: 'dist/sidebar/index.js',
|
||||
format: 'iife', // apparently iife is safe for browsers (safer than cjs)
|
||||
platform: 'browser',
|
||||
external: ['vscode'],
|
||||
}).catch(() => process.exit(1));
|
||||
@@ -1,141 +0,0 @@
|
||||
{
|
||||
"name": "void",
|
||||
"displayName": "Void",
|
||||
"description": "",
|
||||
"version": "0.0.1",
|
||||
"engines": {
|
||||
"vscode": "^1.89.0"
|
||||
},
|
||||
"categories": [
|
||||
"Other"
|
||||
],
|
||||
"activationEvents": [],
|
||||
"main": "./out/extension.js",
|
||||
"contributes": {
|
||||
"configuration": {
|
||||
"title": "API Keys",
|
||||
"properties": {
|
||||
"void.whichApi": {
|
||||
"type": "string",
|
||||
"default": "anthropic",
|
||||
"description": "Choose a model to use (anthropic | openai | greptile | ollama)"
|
||||
},
|
||||
"void.anthropicApiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Anthropic API Key"
|
||||
},
|
||||
"void.openAIApiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "OpenAI API Key"
|
||||
},
|
||||
"void.greptileApiKey": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Greptile API Key"
|
||||
},
|
||||
"void.githubPAT": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Greptile - Github PAT (gives Greptile access to your repo)"
|
||||
},
|
||||
"void.ollamaSettings": {
|
||||
"type": "string",
|
||||
"default": "",
|
||||
"description": "Ollama settings (coming soon...)"
|
||||
}
|
||||
}
|
||||
},
|
||||
"commands": [
|
||||
{
|
||||
"command": "void.ctrl+l",
|
||||
"title": "Show Sidebar"
|
||||
},
|
||||
{
|
||||
"command": "void.ctrl+k",
|
||||
"title": "Show Selection Lens"
|
||||
},
|
||||
{
|
||||
"command": "void.approveDiff",
|
||||
"title": "Approve Diff"
|
||||
},
|
||||
{
|
||||
"command": "void.discardDiff",
|
||||
"title": "Discard Diff"
|
||||
}
|
||||
],
|
||||
"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"
|
||||
}
|
||||
]
|
||||
},
|
||||
"scripts": {
|
||||
"vscode:prepublish": "npm run compile",
|
||||
"watch": "tsc -watch -p ./",
|
||||
"build": "rimraf dist && node build-tsx.js && node build-css.js",
|
||||
"pretest": "tsc -p ./ && eslint src --ext ts",
|
||||
"test": "vscode-test"
|
||||
},
|
||||
"devDependencies": {
|
||||
"@eslint/js": "^9.9.1",
|
||||
"@types/diff": "^5.2.2",
|
||||
"@types/jest": "^29.5.12",
|
||||
"@types/mocha": "^10.0.8",
|
||||
"@types/node": "^22.5.1",
|
||||
"@types/react": "^18.3.4",
|
||||
"@types/react-dom": "^18.3.0",
|
||||
"@types/vscode": "1.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": "^6.0.0-beta",
|
||||
"esbuild": "^0.23.1",
|
||||
"eslint": "^8.57.0",
|
||||
"eslint-plugin-react": "^7.35.1",
|
||||
"eslint-plugin-react-hooks": "^4.6.2",
|
||||
"globals": "^15.9.0",
|
||||
"marked": "^14.1.0",
|
||||
"ollama": "^0.5.8",
|
||||
"postcss": "^8.4.41",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-markdown": "^9.0.1",
|
||||
"rimraf": "^6.0.1",
|
||||
"tailwindcss": "^3.4.10",
|
||||
"typescript": "5.5.4",
|
||||
"typescript-eslint": "^8.3.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.27.1",
|
||||
"openai": "^4.57.0"
|
||||
}
|
||||
}
|
||||
@@ -1,186 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { SuggestedEdit } from './getDiffedLines';
|
||||
|
||||
// each diff on the user's screen right now
|
||||
type DiffType = {
|
||||
diffid: number,
|
||||
lenses: vscode.CodeLens[],
|
||||
greenRange: vscode.Range,
|
||||
originalCode: string, // If a revert happens, we replace the greenRange with this content.
|
||||
}
|
||||
|
||||
// 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:...
|
||||
})
|
||||
|
||||
export class ApprovalCodeLensProvider implements vscode.CodeLensProvider {
|
||||
|
||||
private _diffsOfDocument: { [docUriStr: string]: DiffType[] } = {};
|
||||
private _computedLensesOfDocument: { [docUriStr: string]: vscode.CodeLens[] } = {} // computed from diffsOfDocument[docUriStr].lenses
|
||||
private _diffidPool = 0
|
||||
|
||||
private _onDidChangeCodeLenses: vscode.EventEmitter<void> = new vscode.EventEmitter<void>(); // signals a UI refresh on .fire() events
|
||||
|
||||
private _weAreEditing: boolean = false
|
||||
|
||||
// used internally by vscode
|
||||
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._computedLensesOfDocument[docUriStr]
|
||||
}
|
||||
|
||||
// declared by us, registered with vscode.languages.registerCodeLensProvider()
|
||||
constructor() {
|
||||
// this acts as a useEffect. Every time text changes, clear the diffs in this editor
|
||||
vscode.workspace.onDidChangeTextDocument((e) => {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
if (this._weAreEditing)
|
||||
return
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
this._diffsOfDocument[docUriStr].splice(0) // clear diffs
|
||||
editor.setDecorations(greenDecoration, []) // clear decorations
|
||||
this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute
|
||||
this._onDidChangeCodeLenses.fire() // refresh
|
||||
})
|
||||
}
|
||||
|
||||
// used by us only
|
||||
private refreshLenses = (editor: vscode.TextEditor, docUriStr: string) => {
|
||||
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange)) // refresh highlighting
|
||||
this._computedLensesOfDocument[docUriStr] = this._diffsOfDocument[docUriStr].flatMap(diff => diff.lenses) // recompute _computedLensesOfDocument (can optimize this later)
|
||||
this._onDidChangeCodeLenses.fire() // fire event for vscode to refresh lenses
|
||||
}
|
||||
|
||||
// used by us only
|
||||
public async addNewApprovals(editor: vscode.TextEditor, suggestedEdits: SuggestedEdit[]) {
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
if (!this._diffsOfDocument[docUriStr])
|
||||
this._diffsOfDocument[docUriStr] = []
|
||||
if (!this._computedLensesOfDocument[docUriStr])
|
||||
this._computedLensesOfDocument[docUriStr] = []
|
||||
|
||||
|
||||
// 1. convert suggested edits (which are described using line numbers) into actual edits (described using vscode.Range, vscode.Uri)
|
||||
// must do this before adding codelenses or highlighting so that codelens and highlights will apply to the fresh code and not the old code
|
||||
// apply changes in reverse order so additions don't push down the line numbers of the next edit
|
||||
let workspaceEdit = new vscode.WorkspaceEdit();
|
||||
for (let i = suggestedEdits.length - 1; i > -1; i -= 1) {
|
||||
let suggestedEdit = suggestedEdits[i]
|
||||
|
||||
let greenRange: vscode.Range
|
||||
|
||||
// INSERTIONS (e.g. {originalStartLine: 0, originalEndLine: -1})
|
||||
if (suggestedEdit.originalStartLine > suggestedEdit.originalEndLine) {
|
||||
const originalPosition = new vscode.Position(suggestedEdit.originalStartLine, 0)
|
||||
workspaceEdit.insert(docUri, originalPosition, suggestedEdit.newContent + '\n') // add back in the line we deleted when we made the startline->endline range go negative
|
||||
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine + 1, 0)
|
||||
}
|
||||
// DELETIONS
|
||||
else if (suggestedEdit.startLine > suggestedEdit.endLine) {
|
||||
const deleteRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine + 1, 0)
|
||||
workspaceEdit.delete(docUri, deleteRange)
|
||||
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.startLine, 0)
|
||||
suggestedEdit.originalContent += '\n' // add back in the line we deleted when we made the startline->endline range go negative
|
||||
}
|
||||
// REPLACEMENTS
|
||||
else {
|
||||
const originalRange = new vscode.Range(suggestedEdit.originalStartLine, 0, suggestedEdit.originalEndLine, Number.MAX_SAFE_INTEGER)
|
||||
workspaceEdit.replace(docUri, originalRange, suggestedEdit.newContent)
|
||||
greenRange = new vscode.Range(suggestedEdit.startLine, 0, suggestedEdit.endLine, Number.MAX_SAFE_INTEGER)
|
||||
}
|
||||
|
||||
this._diffsOfDocument[docUriStr].push({
|
||||
diffid: this._diffidPool,
|
||||
greenRange: greenRange,
|
||||
originalCode: suggestedEdit.originalContent,
|
||||
lenses: [
|
||||
new vscode.CodeLens(greenRange, { title: 'Accept', command: 'void.approveDiff', arguments: [{ diffid: this._diffidPool }] }),
|
||||
new vscode.CodeLens(greenRange, { title: 'Reject', command: 'void.discardDiff', arguments: [{ diffid: this._diffidPool }] })
|
||||
]
|
||||
});
|
||||
this._diffidPool += 1
|
||||
}
|
||||
|
||||
this._weAreEditing = true
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
await vscode.workspace.save(docUri)
|
||||
this._weAreEditing = false
|
||||
|
||||
// refresh
|
||||
this.refreshLenses(editor, docUriStr)
|
||||
|
||||
console.log('diffs after added:', this._diffsOfDocument[docUriStr])
|
||||
}
|
||||
|
||||
// called on void.approveDiff
|
||||
public async approveDiff({ diffid }: { diffid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get index of this diff in diffsOfDocument
|
||||
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (index === -1) {
|
||||
console.error('Error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
|
||||
return
|
||||
}
|
||||
|
||||
// remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
|
||||
this._diffsOfDocument[docUriStr].splice(index, 1)
|
||||
|
||||
// refresh
|
||||
this.refreshLenses(editor, docUriStr)
|
||||
}
|
||||
|
||||
|
||||
// called on void.discardDiff
|
||||
public async discardDiff({ diffid }: { diffid: number }) {
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
const docUri = editor.document.uri
|
||||
const docUriStr = docUri.toString()
|
||||
|
||||
// get index of this diff in diffsOfDocument
|
||||
const index = this._diffsOfDocument[docUriStr].findIndex(diff => diff.diffid === diffid);
|
||||
if (index === -1) {
|
||||
console.error('Void error: DiffID could not be found: ', diffid, this._diffsOfDocument[docUriStr])
|
||||
return
|
||||
}
|
||||
|
||||
const { greenRange: range, lenses, originalCode } = this._diffsOfDocument[docUriStr][index] // do this before we splice and mess up index
|
||||
|
||||
// remove this diff from the diffsOfDocument[docStr] (can change this behavior in future if add something like history)
|
||||
this._diffsOfDocument[docUriStr].splice(index, 1)
|
||||
|
||||
// clear the decoration in this diffs range
|
||||
editor.setDecorations(greenDecoration, this._diffsOfDocument[docUriStr].map(diff => diff.greenRange))
|
||||
|
||||
// REVERT THE CHANGE (this is the only part that's different from approveDiff)
|
||||
let workspaceEdit = new vscode.WorkspaceEdit();
|
||||
workspaceEdit.replace(docUri, range, originalCode);
|
||||
this._weAreEditing = true
|
||||
await vscode.workspace.applyEdit(workspaceEdit)
|
||||
await vscode.workspace.save(docUri)
|
||||
this._weAreEditing = false
|
||||
|
||||
// refresh
|
||||
this.refreshLenses(editor, docUriStr)
|
||||
}
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
export class CtrlKCodeLensProvider implements vscode.CodeLensProvider {
|
||||
|
||||
private codelensesOfDocument: { [documentUri: string]: vscode.CodeLens[] } = {};
|
||||
|
||||
// only called by vscode's internals
|
||||
public provideCodeLenses(document: vscode.TextDocument, token: vscode.CancellationToken): vscode.ProviderResult<vscode.CodeLens[]> {
|
||||
const docUri = document.uri.toString()
|
||||
return this.codelensesOfDocument[docUri];
|
||||
}
|
||||
|
||||
// only called by us
|
||||
public addNewCodeLens(document: vscode.TextDocument, selection: vscode.Selection) {
|
||||
|
||||
const docUri = document.uri.toString()
|
||||
|
||||
if (!this.codelensesOfDocument[docUri])
|
||||
this.codelensesOfDocument[docUri] = []
|
||||
|
||||
// if any other codelens intersects with the selection, don't do it (and have the user now focus that codelens)
|
||||
for (let lens of this.codelensesOfDocument[docUri]) {
|
||||
if (lens.range.intersection(selection))
|
||||
return
|
||||
}
|
||||
|
||||
this.codelensesOfDocument[docUri] = [
|
||||
...this.codelensesOfDocument[docUri],
|
||||
new vscode.CodeLens(new vscode.Range(selection.start.line, 0, selection.end.line, Infinity), { title: '', command: '' })];
|
||||
}
|
||||
}
|
||||
@@ -1,78 +0,0 @@
|
||||
// renders the code from `src/sidebar`
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
|
||||
function getNonce() {
|
||||
let text = "";
|
||||
const possible = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789";
|
||||
for (let i = 0; i < 32; i++) {
|
||||
text += possible.charAt(Math.floor(Math.random() * possible.length));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export class SidebarWebviewProvider implements vscode.WebviewViewProvider {
|
||||
public static readonly viewId = 'void.viewnumberone';
|
||||
|
||||
public webview: Promise<vscode.Webview> // used to send messages to the webview
|
||||
|
||||
private readonly _extensionUri: vscode.Uri
|
||||
private _res: (c: vscode.Webview) => void // used to resolve the webview
|
||||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
// const extensionPath = context.extensionPath // the directory where the extension is installed, might be useful later, not sure for what though... was included in webviewProvider code
|
||||
this._extensionUri = context.extensionUri
|
||||
|
||||
let temp_res: typeof this._res | undefined = undefined
|
||||
this.webview = new Promise((res, rej) => { temp_res = res })
|
||||
if (!temp_res) throw new Error("sidebar provider: resolver was undefined")
|
||||
this._res = temp_res
|
||||
}
|
||||
|
||||
// called internally by vscode
|
||||
resolveWebviewView(
|
||||
webviewView: vscode.WebviewView,
|
||||
context: vscode.WebviewViewResolveContext,
|
||||
token: vscode.CancellationToken,
|
||||
) {
|
||||
|
||||
const webview = webviewView.webview
|
||||
|
||||
webview.options = {
|
||||
enableScripts: true,
|
||||
localResourceRoots: [this._extensionUri]
|
||||
};
|
||||
|
||||
// This allows us to use React in vscode
|
||||
// when you run `npm run build`, we take the React code in the `sidebar` folder
|
||||
// and compile it into `dist/sidebar/index.js` and `dist/sidebar/styles.css`
|
||||
// we render that code here
|
||||
const rootPath = this._extensionUri;
|
||||
const scriptUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/index.js'));
|
||||
const stylesUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath, 'dist/sidebar/styles.css'));
|
||||
const rootUri = webview.asWebviewUri(vscode.Uri.joinPath(rootPath));
|
||||
|
||||
const nonce = getNonce(); // only scripts with the nonce are allowed to run, this is a recommended security measure
|
||||
|
||||
|
||||
const allowed_urls = ['https://api.anthropic.com', 'https://api.openai.com', 'https://api.greptile.com']
|
||||
webview.html = `<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Custom View</title>
|
||||
<meta http-equiv="Content-Security-Policy" content="default-src 'self'; connect-src ${allowed_urls.join(' ')}; img-src vscode-resource: https:; script-src 'nonce-${nonce}';style-src vscode-resource: 'unsafe-inline' http: https: data:;">
|
||||
<base href="${rootUri}/">
|
||||
<link href="${stylesUri}" rel="stylesheet">
|
||||
</head>
|
||||
<body>
|
||||
<div id="root"></div>
|
||||
<script nonce="${nonce}" src="${scriptUri}"></script>
|
||||
</body>
|
||||
</html>`;
|
||||
|
||||
|
||||
this._res(webview);
|
||||
}
|
||||
}
|
||||
@@ -1,285 +0,0 @@
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import OpenAI from 'openai';
|
||||
|
||||
// import ollama from 'ollama'
|
||||
|
||||
export type ApiConfig = {
|
||||
anthropic: {
|
||||
apikey: string,
|
||||
},
|
||||
openai: {
|
||||
apikey: string
|
||||
},
|
||||
greptile: {
|
||||
apikey: string,
|
||||
githubPAT: string,
|
||||
repoinfo: {
|
||||
remote: string, // e.g. 'github'
|
||||
repository: string, // e.g. 'voideditor/void'
|
||||
branch: string // e.g. 'main'
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
// TODO
|
||||
},
|
||||
whichApi: string
|
||||
}
|
||||
|
||||
|
||||
|
||||
type OnText = (newText: string, fullText: string) => void
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'user' | 'assistant',
|
||||
content: string
|
||||
}
|
||||
|
||||
type SendLLMMessageFnTypeInternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: (input: string) => void,
|
||||
apiConfig: ApiConfig,
|
||||
})
|
||||
=> {
|
||||
abort: () => void
|
||||
}
|
||||
|
||||
type SendLLMMessageFnTypeExternal = (params: {
|
||||
messages: LLMMessage[],
|
||||
onText: OnText,
|
||||
onFinalMessage: (input: string) => void,
|
||||
apiConfig: ApiConfig | null,
|
||||
})
|
||||
=> {
|
||||
abort: () => void
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// Claude
|
||||
const sendClaudeMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: apiConfig.anthropic.apikey, dangerouslyAllowBrowser: true }); // defaults to process.env["ANTHROPIC_API_KEY"]
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
model: "claude-3-5-sonnet-20240620",
|
||||
max_tokens: 1024,
|
||||
messages: messages,
|
||||
});
|
||||
|
||||
let did_abort = false
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
if (did_abort) return
|
||||
onText(newText, fullText)
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
if (did_abort) return
|
||||
// stringify the response's content
|
||||
let content = claude_response.content.map(c => { if (c.type === 'text') { return c.text } }).join('\n');
|
||||
onFinalMessage(content)
|
||||
})
|
||||
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
const abort = () => {
|
||||
// stream.abort() // this doesnt appear to do anything, but it should try to stop claude from generating anymore
|
||||
did_abort = true
|
||||
}
|
||||
|
||||
return { abort }
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
|
||||
// OpenAI
|
||||
const sendOpenAIMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
|
||||
let did_abort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort: () => void = () => { did_abort = true }
|
||||
|
||||
const openai = new OpenAI({ apiKey: apiConfig.openai.apikey, dangerouslyAllowBrowser: true });
|
||||
|
||||
openai.chat.completions.create({
|
||||
model: 'gpt-4o-2024-08-06',
|
||||
messages: messages,
|
||||
stream: true,
|
||||
})
|
||||
.then(async response => {
|
||||
abort = () => {
|
||||
// response.controller.abort() // this isn't needed now, to keep consistency with claude will leave it commented
|
||||
did_abort = true;
|
||||
}
|
||||
// when receive text
|
||||
try {
|
||||
for await (const chunk of response) {
|
||||
if (did_abort) return;
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText(newText, fullText);
|
||||
}
|
||||
onFinalMessage(fullText);
|
||||
}
|
||||
// when error/fail
|
||||
catch (error) {
|
||||
console.error('Error in OpenAI stream:', error);
|
||||
onFinalMessage(fullText);
|
||||
}
|
||||
// when we get the final message on this stream
|
||||
onFinalMessage(fullText)
|
||||
})
|
||||
return { abort };
|
||||
};
|
||||
|
||||
|
||||
|
||||
// Greptile
|
||||
// https://docs.greptile.com/api-reference/query
|
||||
// https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
|
||||
let did_abort = false
|
||||
let fullText = ''
|
||||
|
||||
// if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
let abort: () => void = () => { did_abort = true }
|
||||
|
||||
|
||||
fetch('https://api.greptile.com/v2/query', {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
"Authorization": `Bearer ${apiConfig.greptile.apikey}`,
|
||||
"X-Github-Token": `${apiConfig.greptile.githubPAT}`,
|
||||
"Content-Type": `application/json`,
|
||||
},
|
||||
body: JSON.stringify({
|
||||
messages,
|
||||
stream: true,
|
||||
repositories: [apiConfig.greptile.repoinfo]
|
||||
}),
|
||||
})
|
||||
// this is {message}\n{message}\n{message}...\n
|
||||
.then(async response => {
|
||||
const text = await response.text()
|
||||
console.log('got greptile', text)
|
||||
return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
})
|
||||
// TODO make this actually stream, right now it just sends one message at the end
|
||||
.then(async responseArr => {
|
||||
if (did_abort)
|
||||
return
|
||||
|
||||
for (let response of responseArr) {
|
||||
|
||||
const type: string = response['type']
|
||||
const message = response['message']
|
||||
|
||||
// when receive text
|
||||
if (type === 'message') {
|
||||
fullText += message
|
||||
onText(message, fullText)
|
||||
}
|
||||
else if (type === 'sources') {
|
||||
const { filepath, linestart, lineend } = message as { filepath: string, linestart: number | null, lineend: number | null }
|
||||
fullText += filepath
|
||||
onText(filepath, fullText)
|
||||
}
|
||||
// type: 'status' with an empty 'message' means last message
|
||||
else if (type === 'status') {
|
||||
if (!message) {
|
||||
onFinalMessage(fullText)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
})
|
||||
.catch(e => {
|
||||
console.error('Error in Greptile stream:', e);
|
||||
onFinalMessage(fullText);
|
||||
|
||||
});
|
||||
|
||||
return { abort }
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
|
||||
export const sendLLMMessage: SendLLMMessageFnTypeExternal = ({ messages, onText, onFinalMessage, apiConfig }) => {
|
||||
if (!apiConfig) return { abort: () => { } }
|
||||
|
||||
const whichApi = apiConfig.whichApi
|
||||
|
||||
if (whichApi === 'anthropic') {
|
||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig })
|
||||
}
|
||||
else if (whichApi === 'openai') {
|
||||
return sendOpenAIMsg({ messages, onText, onFinalMessage, apiConfig })
|
||||
}
|
||||
else if (whichApi === 'greptile') {
|
||||
return sendGreptileMsg({ messages, onText, onFinalMessage, apiConfig })
|
||||
}
|
||||
else if (whichApi === 'ollama') {
|
||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
|
||||
}
|
||||
else {
|
||||
console.error(`Error: whichApi was ${whichApi}, which is not recognized!`)
|
||||
return sendClaudeMsg({ messages, onText, onFinalMessage, apiConfig }) // TODO
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
// Ollama
|
||||
// const sendOllamaMsg: sendMsgFnType = ({ messages, onText, onFinalMessage }) => {
|
||||
|
||||
// let did_abort = false
|
||||
// let fullText = ''
|
||||
|
||||
// // if abort is called, onFinalMessage is NOT called, and no later onTexts are called either
|
||||
// let abort: () => void = () => {
|
||||
// did_abort = true
|
||||
// }
|
||||
|
||||
// ollama.chat({ model: 'llama3.1', messages: messages, stream: true })
|
||||
// .then(async response => {
|
||||
|
||||
// abort = () => {
|
||||
// // response.abort() // this isn't needed now, to keep consistency with claude will leave it commented for now
|
||||
// did_abort = true;
|
||||
// }
|
||||
|
||||
// // when receive text
|
||||
// try {
|
||||
// for await (const part of response) {
|
||||
// if (did_abort) return
|
||||
// let newText = part.message.content
|
||||
// fullText += newText
|
||||
// onText(newText, fullText)
|
||||
// }
|
||||
// }
|
||||
// // when error/fail
|
||||
// catch (e) {
|
||||
// onFinalMessage(fullText)
|
||||
// return
|
||||
// }
|
||||
|
||||
// // when we get the final message on this stream
|
||||
// onFinalMessage(fullText)
|
||||
// })
|
||||
|
||||
// return { abort };
|
||||
// };
|
||||
|
||||
@@ -1,153 +0,0 @@
|
||||
import * as vscode from 'vscode';
|
||||
import { WebviewMessage } from './shared_types';
|
||||
import { CtrlKCodeLensProvider } from './CtrlKCodeLensProvider';
|
||||
import { getDiffedLines } from './getDiffedLines';
|
||||
import { ApprovalCodeLensProvider } from './ApprovalCodeLensProvider';
|
||||
import { SidebarWebviewProvider } from './SidebarWebviewProvider';
|
||||
import { ApiConfig } from './common/sendLLMMessage';
|
||||
|
||||
const readFileContentOfUri = async (uri: vscode.Uri) => {
|
||||
return Buffer.from(await vscode.workspace.fs.readFile(uri)).toString('utf8').replace(/\r\n/g, '\n'); // must remove windows \r or every line will appear different because of it
|
||||
}
|
||||
|
||||
|
||||
const getApiConfig = () => {
|
||||
const apiConfig: ApiConfig = {
|
||||
anthropic: { apikey: vscode.workspace.getConfiguration('void').get('anthropicApiKey') ?? '' },
|
||||
openai: { apikey: vscode.workspace.getConfiguration('void').get('openAIApiKey') ?? '' },
|
||||
greptile: {
|
||||
apikey: vscode.workspace.getConfiguration('void').get('greptileApiKey') ?? '',
|
||||
githubPAT: vscode.workspace.getConfiguration('void').get('githubPAT') ?? '',
|
||||
repoinfo: {
|
||||
remote: 'github',
|
||||
repository: 'TODO',
|
||||
branch: 'main'
|
||||
}
|
||||
},
|
||||
ollama: {
|
||||
// apikey: vscode.workspace.getConfiguration('void').get('ollamaSettings') ?? '',
|
||||
},
|
||||
whichApi: vscode.workspace.getConfiguration('void').get('whichApi') ?? ''
|
||||
}
|
||||
return apiConfig
|
||||
}
|
||||
|
||||
export function activate(context: vscode.ExtensionContext) {
|
||||
|
||||
// 1. Mount the chat sidebar
|
||||
const webviewProvider = new SidebarWebviewProvider(context);
|
||||
context.subscriptions.push(
|
||||
vscode.window.registerWebviewViewProvider(SidebarWebviewProvider.viewId, webviewProvider, { webviewOptions: { retainContextWhenHidden: true } })
|
||||
);
|
||||
|
||||
// 2. Activate the sidebar on ctrl+l
|
||||
context.subscriptions.push(
|
||||
vscode.commands.registerCommand('void.ctrl+l', () => {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor)
|
||||
return
|
||||
|
||||
// show the sidebar
|
||||
vscode.commands.executeCommand('workbench.view.extension.voidViewContainer');
|
||||
// vscode.commands.executeCommand('vscode.moveViewToPanel', CustomViewProvider.viewId); // move to aux bar
|
||||
|
||||
// get the text the user is selecting
|
||||
const selectionStr = editor.document.getText(editor.selection);
|
||||
|
||||
// get the range of the selection
|
||||
const selectionRange = editor.selection;
|
||||
|
||||
// get the file the user is in
|
||||
const filePath = editor.document.uri;
|
||||
|
||||
// send message to the webview (Sidebar.tsx)
|
||||
webviewProvider.webview.then(webview => webview.postMessage({ type: 'ctrl+l', selection: { selectionStr, selectionRange, filePath } } satisfies WebviewMessage));
|
||||
})
|
||||
);
|
||||
|
||||
// 3. Show an approve/reject codelens above each change
|
||||
const approvalCodeLensProvider = new ApprovalCodeLensProvider();
|
||||
context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', approvalCodeLensProvider));
|
||||
|
||||
// 4. Add approve/reject commands
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.approveDiff', async (params) => {
|
||||
approvalCodeLensProvider.approveDiff(params)
|
||||
}));
|
||||
context.subscriptions.push(vscode.commands.registerCommand('void.discardDiff', async (params) => {
|
||||
approvalCodeLensProvider.discardDiff(params)
|
||||
}));
|
||||
|
||||
|
||||
// 5.
|
||||
webviewProvider.webview.then(
|
||||
webview => {
|
||||
|
||||
// when config changes, send it to the sidebar
|
||||
vscode.workspace.onDidChangeConfiguration(e => {
|
||||
if (e.affectsConfiguration('void')) {
|
||||
const apiConfig = getApiConfig()
|
||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
// Receive messages in the extension from the sidebar webview (messages are sent using `postMessage`)
|
||||
webview.onDidReceiveMessage(async (m: WebviewMessage) => {
|
||||
|
||||
if (m.type === 'requestFiles') {
|
||||
|
||||
// get contents of all file paths
|
||||
const files = await Promise.all(
|
||||
m.filepaths.map(async (filepath) => ({ filepath, content: await readFileContentOfUri(filepath) }))
|
||||
)
|
||||
|
||||
// send contents to webview
|
||||
webview.postMessage({ type: 'files', files, } satisfies WebviewMessage)
|
||||
|
||||
} else if (m.type === 'applyCode') {
|
||||
|
||||
const editor = vscode.window.activeTextEditor
|
||||
if (!editor) {
|
||||
vscode.window.showInformationMessage('No active editor!')
|
||||
return
|
||||
}
|
||||
const oldContents = await readFileContentOfUri(editor.document.uri)
|
||||
const suggestedEdits = getDiffedLines(oldContents, m.code)
|
||||
await approvalCodeLensProvider.addNewApprovals(editor, suggestedEdits)
|
||||
}
|
||||
else if (m.type === 'getApiConfig') {
|
||||
|
||||
const apiConfig = getApiConfig()
|
||||
console.log('Api config:', apiConfig)
|
||||
|
||||
webview.postMessage({ type: 'apiConfig', apiConfig } satisfies WebviewMessage)
|
||||
|
||||
}
|
||||
else {
|
||||
|
||||
console.error('unrecognized command', m.type, m)
|
||||
}
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
|
||||
|
||||
|
||||
// Gets called when user presses ctrl + k (mounts ctrl+k-style codelens)
|
||||
// TODO need to build this
|
||||
// const ctrlKCodeLensProvider = new CtrlKCodeLensProvider();
|
||||
// context.subscriptions.push(vscode.languages.registerCodeLensProvider('*', ctrlKCodeLensProvider));
|
||||
// context.subscriptions.push(
|
||||
// vscode.commands.registerCommand('void.ctrl+k', () => {
|
||||
// const editor = vscode.window.activeTextEditor;
|
||||
// if (!editor)
|
||||
// return
|
||||
// ctrlKCodeLensProvider.addNewCodeLens(editor.document, editor.selection);
|
||||
// // vscode.commands.executeCommand('editor.action.showHover'); // apparently this refreshes the codelenses by having the internals call provideCodeLenses
|
||||
// })
|
||||
// )
|
||||
|
||||
}
|
||||
|
||||
@@ -1,91 +0,0 @@
|
||||
import { diffLines, Change } from 'diff';
|
||||
|
||||
export type SuggestedEdit = {
|
||||
// start/end of current file
|
||||
startLine: number;
|
||||
endLine: number;
|
||||
|
||||
// start/end of original file
|
||||
originalStartLine: number,
|
||||
originalEndLine: number,
|
||||
|
||||
// original content (originalfile[originalStart...originalEnd])
|
||||
originalContent: string;
|
||||
newContent: string;
|
||||
}
|
||||
|
||||
export function getDiffedLines(oldStr: string, newStr: string) {
|
||||
// an ordered list of every original line, line added to the new file, and line removed from the old file (order is unambiguous, think about it)
|
||||
const lineByLineChanges: Change[] = diffLines(oldStr, newStr);
|
||||
console.debug('Line by line changes', lineByLineChanges)
|
||||
|
||||
lineByLineChanges.push({ value: '' }) // add a dummy so we flush any streaks we haven't yet at the very end (!line.added && !line.removed)
|
||||
|
||||
let oldFileLineNum: number = 0;
|
||||
let newFileLineNum: number = 0;
|
||||
|
||||
let streakStartInNewFile: number | undefined = undefined
|
||||
let streakStartInOldFile: number | undefined = undefined
|
||||
|
||||
let oldStrLines = oldStr.split('\n')
|
||||
let newStrLines = newStr.split('\n')
|
||||
|
||||
const replacements: SuggestedEdit[] = []
|
||||
|
||||
for (let line of lineByLineChanges) {
|
||||
// no change on this line
|
||||
if (!line.added && !line.removed) {
|
||||
// if we were on a streak, add it
|
||||
if (streakStartInNewFile !== undefined) {
|
||||
|
||||
const startLine = streakStartInNewFile
|
||||
const endLine = newFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
const newContent = newStrLines.slice(startLine, endLine + 1).join('\n')
|
||||
|
||||
const originalStartLine = streakStartInOldFile!
|
||||
const originalEndLine = oldFileLineNum - 1 // don't include current line, the edit was up to this line but not including it
|
||||
const originalContent = oldStrLines.slice(originalStartLine, originalEndLine + 1).join('\n')
|
||||
|
||||
const replacement: SuggestedEdit = { startLine, endLine, newContent, originalStartLine, originalEndLine, originalContent }
|
||||
|
||||
replacements.push(replacement)
|
||||
streakStartInNewFile = undefined
|
||||
streakStartInOldFile = undefined
|
||||
}
|
||||
|
||||
oldFileLineNum += line.count ?? 0;
|
||||
newFileLineNum += line.count ?? 0;
|
||||
}
|
||||
|
||||
|
||||
// line was removed from old file
|
||||
else if (line.removed) {
|
||||
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
|
||||
oldFileLineNum += line.count ?? 0 // we processed the line so add 1
|
||||
}
|
||||
|
||||
// line was added to new file
|
||||
else if (line.added) {
|
||||
|
||||
// if we weren't on a streak, start one on this current line num
|
||||
if (streakStartInNewFile === undefined) {
|
||||
streakStartInNewFile = newFileLineNum
|
||||
streakStartInOldFile = oldFileLineNum
|
||||
}
|
||||
|
||||
newFileLineNum += line.count ?? 0; // we processed the line so add 1
|
||||
}
|
||||
|
||||
} // end for
|
||||
|
||||
console.debug('Replacements', replacements)
|
||||
|
||||
return replacements
|
||||
|
||||
}
|
||||
@@ -1,39 +0,0 @@
|
||||
|
||||
import * as vscode from 'vscode';
|
||||
import { ApiConfig } from './common/sendLLMMessage';
|
||||
|
||||
// a selection is a frozen snapshot
|
||||
type Selection = { selectionStr: string, selectionRange: vscode.Range, filePath: vscode.Uri }
|
||||
|
||||
type File = { filepath: vscode.Uri, content: string }
|
||||
|
||||
type WebviewMessage = (
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'ctrl+l', selection: Selection } // user presses ctrl+l in the editor
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'applyCode', code: string } // user clicks "apply" in the sidebar
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'requestFiles', filepaths: vscode.Uri[] }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'files', files: { filepath: vscode.Uri, content: string }[] }
|
||||
|
||||
// sidebar -> editor
|
||||
| { type: 'getApiConfig' }
|
||||
|
||||
// editor -> sidebar
|
||||
| { type: 'apiConfig', apiConfig: ApiConfig }
|
||||
|
||||
)
|
||||
|
||||
type Command = WebviewMessage['type']
|
||||
|
||||
export {
|
||||
Selection,
|
||||
File,
|
||||
WebviewMessage,
|
||||
Command,
|
||||
}
|
||||
@@ -1,160 +0,0 @@
|
||||
import React, { JSX, useState } from 'react';
|
||||
import { MarkedToken, Token, TokensList } from 'marked';
|
||||
import { awaitVSCodeResponse, getVSCodeAPI } from './getVscodeApi';
|
||||
|
||||
|
||||
// code block with Apply button at top
|
||||
export const BlockCode = ({ text, disableApplyButton = false }: { text: string, disableApplyButton?: boolean }) => {
|
||||
return <div className='py-1'>
|
||||
{disableApplyButton ? null : <div className='text-sm'>
|
||||
<button className='btn btn-secondary px-3 py-1 text-sm rounded-t-sm'
|
||||
onClick={async () => { getVSCodeAPI().postMessage({ type: 'applyCode', code: text }) }}>Apply</button>
|
||||
</div>}
|
||||
<div className={`overflow-x-auto rounded-sm text-vscode-editor-fg bg-vscode-editor-bg ${disableApplyButton ? '' : 'rounded-tl-none'}`}>
|
||||
<pre className='p-3'>
|
||||
{text}
|
||||
</pre>
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const Render = ({ token }: { token: Token }) => {
|
||||
|
||||
// deal with built-in tokens first (assume marked token)
|
||||
const t = token as MarkedToken
|
||||
|
||||
if (t.type === "space") {
|
||||
return <span>{t.raw}</span>;
|
||||
}
|
||||
|
||||
if (t.type === "code") {
|
||||
return <BlockCode text={t.text} />
|
||||
}
|
||||
|
||||
if (t.type === "heading") {
|
||||
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements;
|
||||
return <HeadingTag>{t.text}</HeadingTag>;
|
||||
}
|
||||
|
||||
if (t.type === "table") {
|
||||
return (
|
||||
<table>
|
||||
<thead>
|
||||
<tr>
|
||||
{t.header.map((cell: any, index: number) => (
|
||||
<th key={index} style={{ textAlign: t.align[index] || 'left' }}>
|
||||
{cell.raw}
|
||||
</th>
|
||||
))}
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{t.rows.map((row: any[], rowIndex: number) => (
|
||||
<tr key={rowIndex}>
|
||||
{row.map((cell: any, cellIndex: number) => (
|
||||
<td key={cellIndex} style={{ textAlign: t.align[cellIndex] || 'left' }}>
|
||||
{cell.raw}
|
||||
</td>
|
||||
))}
|
||||
</tr>
|
||||
))}
|
||||
</tbody>
|
||||
</table>
|
||||
);
|
||||
}
|
||||
|
||||
if (t.type === "hr") {
|
||||
return <hr />;
|
||||
}
|
||||
|
||||
if (t.type === "blockquote") {
|
||||
return <blockquote>{t.text}</blockquote>;
|
||||
}
|
||||
|
||||
if (t.type === "list") {
|
||||
|
||||
const ListTag = t.ordered ? 'ol' : 'ul';
|
||||
return (
|
||||
<ListTag start={t.start !== '' ? t.start : undefined}
|
||||
className={`list-inside ${t.ordered ? 'list-decimal' : 'list-disc'}`}
|
||||
>
|
||||
{t.items.map((item, index) => (
|
||||
<li key={index}>
|
||||
{item.task && (
|
||||
<input type="checkbox" checked={item.checked} readOnly />
|
||||
)}
|
||||
{item.text}
|
||||
</li>
|
||||
))}
|
||||
</ListTag>
|
||||
);
|
||||
}
|
||||
|
||||
if (t.type === "paragraph") {
|
||||
return <p>
|
||||
{t.tokens.map((token, index) => (
|
||||
<Render key={index} token={token} />
|
||||
))}
|
||||
</p>;
|
||||
}
|
||||
|
||||
if (t.type === "html") {
|
||||
return <pre>{`<html>`}{t.raw}{`</html>`}</pre>;
|
||||
}
|
||||
|
||||
if (t.type === "text" || t.type === "escape") {
|
||||
return <span>{t.raw}</span>;
|
||||
}
|
||||
|
||||
if (t.type === "def") {
|
||||
return null; // Definitions are typically not rendered
|
||||
}
|
||||
|
||||
if (t.type === "link") {
|
||||
return <a href={t.href} title={t.title ?? undefined}>{t.text}</a>;
|
||||
}
|
||||
|
||||
if (t.type === "image") {
|
||||
return <img src={t.href} alt={t.text} title={t.title ?? undefined} />;
|
||||
}
|
||||
|
||||
if (t.type === "strong") {
|
||||
return <strong>{t.text}</strong>;
|
||||
}
|
||||
|
||||
if (t.type === "em") {
|
||||
return <em>{t.text}</em>;
|
||||
}
|
||||
|
||||
// inline code
|
||||
if (t.type === "codespan") {
|
||||
return <code className='text-vscode-editor-fg bg-vscode-editor-bg px-1 rounded-sm font-mono'>{t.text}</code>;
|
||||
}
|
||||
|
||||
if (t.type === "br") {
|
||||
return <br />;
|
||||
}
|
||||
|
||||
if (t.type === "del") {
|
||||
return <del>{t.text}</del>;
|
||||
}
|
||||
|
||||
|
||||
// default
|
||||
return <div className='bg-orange-50 rounded-sm overflow-hidden'>
|
||||
<span className='text-xs text-orange-500'>Unknown type:</span>
|
||||
{t.raw}
|
||||
</div>;
|
||||
};
|
||||
|
||||
const MarkdownRender = ({ tokens }: { tokens: TokensList }) => {
|
||||
return (
|
||||
<>
|
||||
{tokens.map((token, index) => (
|
||||
<Render key={index} token={token} />
|
||||
))}
|
||||
</>
|
||||
);
|
||||
};
|
||||
|
||||
export default MarkdownRender;
|
||||
@@ -1,324 +0,0 @@
|
||||
import React, { useState, ChangeEvent, useEffect, useRef, useCallback, FormEvent } from "react"
|
||||
import { ApiConfig, LLMMessage, sendLLMMessage } from "../common/sendLLMMessage"
|
||||
import { Command, File, Selection, WebviewMessage } from "../shared_types"
|
||||
import { awaitVSCodeResponse, getVSCodeAPI, resolveAwaitingVSCodeResponse } from "./getVscodeApi"
|
||||
|
||||
import { marked } from 'marked';
|
||||
import MarkdownRender, { BlockCode } from "./MarkdownRender";
|
||||
|
||||
import * as vscode from 'vscode'
|
||||
|
||||
|
||||
const filesStr = (fullFiles: File[]) => {
|
||||
return fullFiles.map(({ filepath, content }) =>
|
||||
`
|
||||
${filepath.fsPath}
|
||||
\`\`\`
|
||||
${content}
|
||||
\`\`\``).join('\n')
|
||||
}
|
||||
|
||||
const userInstructionsStr = (instructions: string, files: File[], selection: Selection | null) => {
|
||||
return `
|
||||
${filesStr(files)}
|
||||
|
||||
${!selection ? '' : `
|
||||
I am currently selecting this code:
|
||||
\`\`\`${selection.selectionStr}\`\`\`
|
||||
`}
|
||||
|
||||
Please edit the code following these instructions:
|
||||
${instructions}
|
||||
|
||||
If you make a change, rewrite the entire file.
|
||||
`; // TODO don't rewrite the whole file on prompt, instead rewrite it when click Apply
|
||||
}
|
||||
|
||||
|
||||
const FilesSelector = ({ files, setFiles }: { files: vscode.Uri[], setFiles: (files: vscode.Uri[]) => void }) => {
|
||||
return files.length !== 0 && <div className='my-2'>
|
||||
Include files:
|
||||
{files.map((filename, i) =>
|
||||
<div key={i} className='flex'>
|
||||
{/* X button on a file */}
|
||||
<button type='button' onClick={() => {
|
||||
let file_index = files.indexOf(filename)
|
||||
setFiles([...files.slice(0, file_index), ...files.slice(file_index + 1, Infinity)])
|
||||
}}>
|
||||
-{' '}<span className='text-gray-500'>{getBasename(filename.fsPath)}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
}
|
||||
|
||||
const IncludedFiles = ({ files }: { files: vscode.Uri[] }) => {
|
||||
return files.length !== 0 && <div className='text-xs my-2'>
|
||||
{files.map((filename, i) =>
|
||||
<div key={i} className='flex'>
|
||||
<button type='button'
|
||||
className='btn btn-secondary pointer-events-none'
|
||||
onClick={() => {
|
||||
// TODO redirect to the document filename.fsPath, when add this remove pointer-events-none
|
||||
}}>
|
||||
-{' '}<span className='text-gray-100'>{getBasename(filename.fsPath)}</span>
|
||||
</button>
|
||||
</div>
|
||||
)}
|
||||
</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 = <>
|
||||
<IncludedFiles files={chatMessage.files} />
|
||||
{chatMessage.selection?.selectionStr && <BlockCode text={chatMessage.selection.selectionStr} disableApplyButton={true} />}
|
||||
{children}
|
||||
</>
|
||||
}
|
||||
else if (role === 'assistant') {
|
||||
const tokens = marked.lexer(children); // https://marked.js.org/using_pro#renderer
|
||||
chatbubbleContents = <MarkdownRender tokens={tokens} /> // sectionsHTML
|
||||
}
|
||||
|
||||
|
||||
return <div className={`${role === 'user' ? 'text-right' : 'text-left'}`}>
|
||||
<div className={`inline-block p-2 rounded-lg space-y-2 ${role === 'user' ? 'bg-vscode-input-bg text-vscode-input-fg' : ''} max-w-full`}>
|
||||
{chatbubbleContents}
|
||||
</div>
|
||||
</div>
|
||||
}
|
||||
|
||||
const 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]
|
||||
}
|
||||
|
||||
type ChatMessage = {
|
||||
role: 'user'
|
||||
content: string, // content sent to the llm
|
||||
displayContent: string, // content displayed to user
|
||||
selection: Selection | null, // the user's selection
|
||||
files: vscode.Uri[], // the files sent in the message
|
||||
} | {
|
||||
role: 'assistant',
|
||||
content: string, // content received from LLM
|
||||
displayContent: string // content displayed to user (this is the same as content for now)
|
||||
}
|
||||
|
||||
|
||||
// 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
|
||||
}
|
||||
|
||||
|
||||
|
||||
const Sidebar = () => {
|
||||
|
||||
// state of current message
|
||||
const [selection, setSelection] = useState<Selection | 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 [chatMessageHistory, setChatHistory] = useState<ChatMessage[]>([])
|
||||
const [messageStream, setMessageStream] = useState('')
|
||||
const [isLoading, setIsLoading] = useState(false)
|
||||
|
||||
const abortFnRef = useRef<(() => void) | null>(null)
|
||||
|
||||
const [apiConfig, setApiConfig] = useState<ApiConfig | null>(null)
|
||||
|
||||
// get Api Config on mount
|
||||
useEffect(() => {
|
||||
getVSCodeAPI().postMessage({ type: 'getApiConfig' })
|
||||
}, [])
|
||||
|
||||
// Receive messages from the extension
|
||||
useEffect(() => {
|
||||
const listener = (event: MessageEvent) => {
|
||||
|
||||
const m = event.data as WebviewMessage;
|
||||
// resolve any awaiting promises
|
||||
// eg. it will resolve the promise below for `await VSCodeResponse('files')`
|
||||
resolveAwaitingVSCodeResponse(m)
|
||||
|
||||
// if user pressed ctrl+l, add their selection to the sidebar
|
||||
if (m.type === 'ctrl+l') {
|
||||
|
||||
setSelection(m.selection)
|
||||
|
||||
const filepath = m.selection.filePath
|
||||
|
||||
// add file if it's not a duplicate
|
||||
if (!files.find(f => f.fsPath === filepath.fsPath)) setFiles(files => [...files, filepath])
|
||||
|
||||
}
|
||||
// when get apiConfig, set
|
||||
else if (m.type === 'apiConfig') {
|
||||
setApiConfig(m.apiConfig)
|
||||
}
|
||||
|
||||
}
|
||||
window.addEventListener('message', listener);
|
||||
return () => { window.removeEventListener('message', listener) }
|
||||
}, [files, selection])
|
||||
|
||||
|
||||
const formRef = useRef<HTMLFormElement | null>(null)
|
||||
const onSubmit = async (e: FormEvent<HTMLFormElement>) => {
|
||||
|
||||
e.preventDefault()
|
||||
if (isLoading) return
|
||||
|
||||
setIsLoading(true)
|
||||
setInstructions('');
|
||||
formRef.current?.reset(); // reset the form's text
|
||||
setSelection(null)
|
||||
setFiles([])
|
||||
|
||||
// request file content from vscode and await response
|
||||
getVSCodeAPI().postMessage({ type: 'requestFiles', filepaths: files })
|
||||
const relevantFiles = await awaitVSCodeResponse('files')
|
||||
|
||||
// add message to chat history
|
||||
const content = userInstructionsStr(instructions, relevantFiles.files, selection)
|
||||
// console.log('prompt:\n', content)
|
||||
const newHistoryElt: ChatMessage = { role: 'user', content, displayContent: instructions, selection, files }
|
||||
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||
|
||||
// send message to claude
|
||||
let { abort } = sendLLMMessage({
|
||||
messages: [...chatMessageHistory.map(m => ({ role: m.role, content: m.content })), { role: 'user', content }],
|
||||
onText: (newText, fullText) => setMessageStream(fullText),
|
||||
onFinalMessage: (content) => {
|
||||
|
||||
// add assistant's message to chat history
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', content, displayContent: content, }
|
||||
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||
|
||||
// clear selection
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
},
|
||||
apiConfig: apiConfig
|
||||
})
|
||||
abortFnRef.current = abort
|
||||
|
||||
}
|
||||
|
||||
const onStop = useCallback(() => {
|
||||
// abort claude
|
||||
abortFnRef.current?.()
|
||||
|
||||
// if messageStream was not empty, add it to the history
|
||||
const llmContent = messageStream || '(canceled)'
|
||||
const newHistoryElt: ChatMessage = { role: 'assistant', displayContent: messageStream, content: llmContent }
|
||||
setChatHistory(chatMessageHistory => [...chatMessageHistory, newHistoryElt])
|
||||
|
||||
setMessageStream('')
|
||||
setIsLoading(false)
|
||||
|
||||
}, [messageStream])
|
||||
|
||||
//Clear code selection
|
||||
const clearSelection = () => {
|
||||
setSelection(null);
|
||||
};
|
||||
|
||||
return <>
|
||||
<div className="flex flex-col h-screen w-full">
|
||||
<div className="overflow-y-auto overflow-x-hidden space-y-4">
|
||||
{/* previous messages */}
|
||||
{chatMessageHistory.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">
|
||||
{/* selected files */}
|
||||
<FilesSelector files={files} setFiles={setFiles} />
|
||||
{/* selected code */}
|
||||
{!selection?.selectionStr ? null
|
||||
: (
|
||||
<div className="relative">
|
||||
<button
|
||||
onClick={clearSelection}
|
||||
className="absolute top-2 right-2 text-white hover:text-gray-300 z-10"
|
||||
>
|
||||
X
|
||||
</button>
|
||||
<BlockCode text={selection.selectionStr} disableApplyButton={true} />
|
||||
</div>
|
||||
)}
|
||||
</div>
|
||||
<form
|
||||
ref={formRef}
|
||||
className="flex flex-row items-center rounded-md p-2 input"
|
||||
onKeyDown={(e) => { if (e.key === 'Enter' && !e.shiftKey) onSubmit(e) }}
|
||||
|
||||
onSubmit={(e) => {
|
||||
console.log('submit!')
|
||||
e.preventDefault();
|
||||
onSubmit(e)
|
||||
}}>
|
||||
{/* input */}
|
||||
|
||||
<textarea
|
||||
onChange={(e) => { setInstructions(e.target.value) }}
|
||||
className="w-full p-2 leading-tight resize-none max-h-[50vh] overflow-hidden bg-transparent border-none !outline-none"
|
||||
placeholder="Ctrl+L to select"
|
||||
rows={1}
|
||||
onInput={e => { e.currentTarget.style.height = 'auto'; e.currentTarget.style.height = e.currentTarget.scrollHeight + 'px' }} // Adjust height dynamically
|
||||
/>
|
||||
{/* submit button */}
|
||||
{isLoading ?
|
||||
<button
|
||||
onClick={onStop}
|
||||
className="btn btn-primary rounded-r-lg max-h-10 p-2"
|
||||
type='button'
|
||||
>Stop</button>
|
||||
: <button
|
||||
className="btn btn-primary font-bold size-8 flex justify-center items-center rounded-full p-2 max-h-10"
|
||||
disabled={!instructions}
|
||||
type='submit'
|
||||
>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" width="24" height="24" viewBox="0 0 24 24" fill="none" stroke="currentColor" strokeWidth="2" strokeLinecap="round" strokeLinejoin="round">
|
||||
<line x1="12" y1="19" x2="12" y2="5"></line>
|
||||
<polyline points="5 12 12 5 19 12"></polyline>
|
||||
</svg>
|
||||
</button>
|
||||
}
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</>
|
||||
|
||||
}
|
||||
|
||||
export default Sidebar
|
||||
@@ -1,54 +0,0 @@
|
||||
import { Command, WebviewMessage } from "../shared_types";
|
||||
|
||||
|
||||
|
||||
// message -> res[]
|
||||
const awaiting: { [c in Command]: ((res: any) => void)[] } = {
|
||||
"ctrl+l": [],
|
||||
"applyCode": [],
|
||||
"requestFiles": [],
|
||||
"files": [],
|
||||
"apiConfig": [],
|
||||
"getApiConfig": []
|
||||
}
|
||||
|
||||
// use this function to await responses
|
||||
export const awaitVSCodeResponse = <C extends Command>(c: C) => {
|
||||
let result: Promise<WebviewMessage & { type: C }> = new Promise((res, rej) => {
|
||||
awaiting[c].push(res)
|
||||
})
|
||||
return result
|
||||
}
|
||||
|
||||
export const resolveAwaitingVSCodeResponse = (m: WebviewMessage) => {
|
||||
// resolve all promises for this message
|
||||
for (let res of awaiting[m.type]) {
|
||||
res(m)
|
||||
awaiting[m.type].splice(0) // clear the array
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// VS Code exposes the function acquireVsCodeApi() to us, it should only get called once
|
||||
let vsCodeApi: ReturnType<AcquireVsCodeApiType> | undefined;
|
||||
|
||||
type AcquireVsCodeApiType = () => {
|
||||
postMessage(message: WebviewMessage): void;
|
||||
// setState(state: any): void; // getState and setState are made obsolete by us using { retainContextWhenHidden: true }
|
||||
// getState(): any;
|
||||
};
|
||||
|
||||
export function getVSCodeAPI(): ReturnType<AcquireVsCodeApiType> {
|
||||
if (vsCodeApi)
|
||||
return vsCodeApi;
|
||||
|
||||
try {
|
||||
// @ts-expect-error
|
||||
// eslint-disable-next-line no-undef
|
||||
vsCodeApi = acquireVsCodeApi();
|
||||
return vsCodeApi!;
|
||||
} catch (error) {
|
||||
console.error('Failed to acquire VS Code API:', error);
|
||||
throw new Error('This script must be run in a VS Code webview context');
|
||||
}
|
||||
}
|
||||
@@ -1,16 +0,0 @@
|
||||
import * as React from 'react'
|
||||
import * as ReactDOM from 'react-dom/client'
|
||||
import Sidebar from './Sidebar'
|
||||
|
||||
// mount the sidebar on the id="root" element
|
||||
if (typeof document === 'undefined') {
|
||||
console.log('index.tsx error: document was undefined')
|
||||
}
|
||||
|
||||
const rootElement = document.getElementById('root')!
|
||||
console.log('root Element', rootElement)
|
||||
const root = ReactDOM.createRoot(rootElement)
|
||||
root.render(<Sidebar />)
|
||||
|
||||
|
||||
|
||||
@@ -1,35 +0,0 @@
|
||||
@tailwind base;
|
||||
@tailwind components;
|
||||
@tailwind utilities;
|
||||
|
||||
html {
|
||||
font-size: var(--vscode-font-size);
|
||||
}
|
||||
|
||||
.btn {
|
||||
@apply cursor-pointer transition-colors;
|
||||
|
||||
&.btn-primary {
|
||||
@apply bg-vscode-button-bg text-vscode-button-fg;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
&.btn-secondary {
|
||||
@apply bg-vscode-button-secondary-bg text-vscode-button-secondary-fg;
|
||||
|
||||
&:not(:disabled) {
|
||||
@apply hover:bg-vscode-button-secondary-hoverBg;
|
||||
}
|
||||
}
|
||||
|
||||
&:disabled {
|
||||
@apply opacity-75 cursor-not-allowed;
|
||||
}
|
||||
}
|
||||
|
||||
.input {
|
||||
@apply bg-vscode-input-bg text-vscode-input-fg border-vscode-input-border;
|
||||
}
|
||||
@@ -1,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,26 +0,0 @@
|
||||
/** @type {import('tailwindcss').Config} */
|
||||
|
||||
// inject user's vscode theme colors: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
|
||||
module.exports = {
|
||||
content: ["./src/sidebar/**/*.{html,js,ts,jsx,tsx}"],
|
||||
theme: {
|
||||
extend: {
|
||||
colors: {
|
||||
vscode: {
|
||||
'editor-bg': "var(--vscode-editor-background)",
|
||||
'editor-fg': "var(--vscode-editor-foreground)",
|
||||
'input-bg': "var(--vscode-input-background)",
|
||||
'input-fg': "var(--vscode-input-foreground)",
|
||||
'input-border': "var(--vscode-input-border)",
|
||||
'button-fg': "var(--vscode-button-foreground)",
|
||||
'button-bg': "var(--vscode-button-background)",
|
||||
'button-hoverBg': "var(--vscode-button-hoverBackground)",
|
||||
'button-secondary-fg': "var(--vscode-button-secondaryForeground)",
|
||||
'button-secondary-bg': "var(--vscode-button-secondaryBackground)",
|
||||
'button-secondary-hoverBg': "var(--vscode-button-secondaryHoverBackground)",
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
plugins: [],
|
||||
};
|
||||
@@ -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. */
|
||||
}
|
||||
}
|
||||
@@ -1,14 +1,19 @@
|
||||
{
|
||||
"name": "code-oss-dev",
|
||||
"name": "void-dev",
|
||||
"productName": "Void",
|
||||
"version": "1.94.0",
|
||||
"distro": "ffcc24343ac46468a625666e5b9e673971dd1a1f",
|
||||
"distro": "this is a commit number if we want to publish on npm",
|
||||
"homepage": "https://voideditor.com",
|
||||
"author": {
|
||||
"name": "Microsoft Corporation"
|
||||
"name": "Glass Devtools, Inc.",
|
||||
"email": "andrew@voideditor.com"
|
||||
},
|
||||
"license": "MIT",
|
||||
"main": "./out/main",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"buildreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js && cd ../../../../../../../",
|
||||
"watchreact": "cd ./src/vs/workbench/contrib/void/browser/react/ && node build.js --watch && cd ../../../../../../../",
|
||||
"test": "echo Please run any of the test scripts from the scripts folder.",
|
||||
"test-browser": "npx playwright install && node test/unit/browser/index.js",
|
||||
"test-browser-amd": "npx playwright install && node test/unit/browser/index.amd.js",
|
||||
@@ -72,9 +77,13 @@
|
||||
"update-build-ts-version": "npm install typescript@next && tsc -p ./build/tsconfig.build.json"
|
||||
},
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.32.1",
|
||||
"@google/generative-ai": "^0.21.0",
|
||||
"@microsoft/1ds-core-js": "^3.2.13",
|
||||
"@microsoft/1ds-post-js": "^3.2.13",
|
||||
"@parcel/watcher": "2.1.0",
|
||||
"@rrweb/record": "^2.0.0-alpha.17",
|
||||
"@rrweb/types": "^2.0.0-alpha.17",
|
||||
"@vscode/deviceid": "^0.1.1",
|
||||
"@vscode/iconv-lite-umd": "0.7.0",
|
||||
"@vscode/policy-watcher": "^1.1.4",
|
||||
@@ -85,6 +94,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 +106,26 @@
|
||||
"@xterm/addon-webgl": "^0.19.0-beta.64",
|
||||
"@xterm/headless": "^5.6.0-beta.64",
|
||||
"@xterm/xterm": "^5.6.0-beta.64",
|
||||
"ajv": "^8.17.1",
|
||||
"diff": "^7.0.0",
|
||||
"groq-sdk": "^0.9.0",
|
||||
"http-proxy-agent": "^7.0.0",
|
||||
"https-proxy-agent": "^7.0.2",
|
||||
"jschardet": "3.1.3",
|
||||
"kerberos": "2.1.1",
|
||||
"lucide-react": "^0.460.0",
|
||||
"minimist": "^1.2.6",
|
||||
"native-is-elevated": "0.7.0",
|
||||
"native-keymap": "^3.3.5",
|
||||
"native-watchdog": "^1.4.1",
|
||||
"node-pty": "1.1.0-beta21",
|
||||
"ollama": "^0.5.11",
|
||||
"open": "^8.4.2",
|
||||
"openai": "^4.76.1",
|
||||
"posthog-node": "^4.3.1",
|
||||
"react": "^18.3.1",
|
||||
"react-dom": "^18.3.1",
|
||||
"react-syntax-highlighter": "^15.6.1",
|
||||
"tas-client-umd": "0.2.0",
|
||||
"v8-inspect-profiler": "^0.1.1",
|
||||
"vscode-oniguruma": "1.7.0",
|
||||
@@ -119,12 +139,17 @@
|
||||
"@swc/core": "1.3.62",
|
||||
"@types/cookie": "^0.3.3",
|
||||
"@types/debug": "^4.1.5",
|
||||
"@types/diff": "^6.0.0",
|
||||
"@types/eslint": "^9.6.1",
|
||||
"@types/gulp-svgmin": "^1.2.1",
|
||||
"@types/http-proxy-agent": "^2.0.1",
|
||||
"@types/kerberos": "^1.1.2",
|
||||
"@types/minimist": "^1.2.1",
|
||||
"@types/mocha": "^9.1.1",
|
||||
"@types/node": "20.x",
|
||||
"@types/react": "^18.3.12",
|
||||
"@types/react-dom": "^18.3.1",
|
||||
"@types/react-syntax-highlighter": "^15.5.13",
|
||||
"@types/sinon": "^10.0.2",
|
||||
"@types/sinon-test": "^2.4.2",
|
||||
"@types/trusted-types": "^1.0.6",
|
||||
@@ -190,6 +215,7 @@
|
||||
"istanbul-lib-source-maps": "^4.0.1",
|
||||
"istanbul-reports": "^3.1.5",
|
||||
"lazy.js": "^0.4.2",
|
||||
"marked": "^15.0.0",
|
||||
"merge-options": "^1.0.1",
|
||||
"mime": "^1.4.1",
|
||||
"minimatch": "^3.0.4",
|
||||
@@ -197,8 +223,11 @@
|
||||
"mocha": "^10.2.0",
|
||||
"mocha-junit-reporter": "^2.2.1",
|
||||
"mocha-multi-reporters": "^1.5.1",
|
||||
"next": "^15.1.4",
|
||||
"nodemon": "^3.1.9",
|
||||
"npm-run-all": "^4.1.5",
|
||||
"opn": "^6.0.0",
|
||||
"original-fs": "^1.2.0",
|
||||
"os-browserify": "^0.3.0",
|
||||
"p-all": "^1.0.0",
|
||||
"path-browserify": "^1.0.1",
|
||||
@@ -206,16 +235,19 @@
|
||||
"postcss-nesting": "^12.0.2",
|
||||
"pump": "^1.0.1",
|
||||
"rcedit": "^1.1.0",
|
||||
"rimraf": "^2.2.8",
|
||||
"rimraf": "^2.7.1",
|
||||
"scope-tailwind": "^1.0.5",
|
||||
"sinon": "^12.0.1",
|
||||
"sinon-test": "^3.1.3",
|
||||
"source-map": "0.6.1",
|
||||
"source-map-support": "^0.3.2",
|
||||
"style-loader": "^3.3.2",
|
||||
"tailwindcss": "^3.4.14",
|
||||
"ts-loader": "^9.4.2",
|
||||
"ts-node": "^10.9.1",
|
||||
"tsec": "0.2.7",
|
||||
"tslib": "^2.6.3",
|
||||
"tsup": "^8.3.5",
|
||||
"typescript": "^5.7.0-dev.20240903",
|
||||
"util": "^0.12.4",
|
||||
"webpack": "^5.94.0",
|
||||
|
||||
@@ -1,84 +1,38 @@
|
||||
{
|
||||
"nameShort": "Void",
|
||||
"nameLong": "Void",
|
||||
"applicationName": "code-oss",
|
||||
"dataFolderName": ".vscode-oss",
|
||||
"win32MutexName": "vscodeoss",
|
||||
"applicationName": "void",
|
||||
"dataFolderName": ".void-editor",
|
||||
"win32MutexName": "voideditor",
|
||||
"licenseName": "MIT",
|
||||
"licenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
|
||||
"serverLicenseUrl": "https://github.com/microsoft/vscode/blob/main/LICENSE.txt",
|
||||
"licenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
|
||||
"serverLicenseUrl": "https://github.com/voideditor/void/blob/main/LICENSE.txt",
|
||||
"serverGreeting": [],
|
||||
"serverLicense": [],
|
||||
"serverLicensePrompt": "",
|
||||
"serverApplicationName": "code-server-oss",
|
||||
"serverDataFolderName": ".vscode-server-oss",
|
||||
"tunnelApplicationName": "code-tunnel-oss",
|
||||
"win32DirName": "Microsoft Code OSS",
|
||||
"win32NameVersion": "Microsoft Code OSS",
|
||||
"win32RegValueName": "CodeOSS",
|
||||
"win32x64AppId": "{{D77B7E06-80BA-4137-BCF4-654B95CCEBC5}",
|
||||
"win32arm64AppId": "{{D1ACE434-89C5-48D1-88D3-E2991DF85475}",
|
||||
"win32x64UserAppId": "{{CC6B787D-37A0-49E8-AE24-8559A032BE0C}",
|
||||
"win32arm64UserAppId": "{{3AEBF0C8-F733-4AD4-BADE-FDB816D53D7B}",
|
||||
"win32AppUserModelId": "Microsoft.CodeOSS",
|
||||
"win32ShellNameShort": "C&ode - OSS",
|
||||
"win32TunnelServiceMutex": "vscodeoss-tunnelservice",
|
||||
"win32TunnelMutex": "vscodeoss-tunnel",
|
||||
"darwinBundleIdentifier": "com.visualstudio.code.oss",
|
||||
"linuxIconName": "code-oss",
|
||||
"serverApplicationName": "void-server",
|
||||
"serverDataFolderName": ".void-server",
|
||||
"tunnelApplicationName": "void-tunnel",
|
||||
"win32DirName": "Void",
|
||||
"win32NameVersion": "Void",
|
||||
"win32RegValueName": "VoidEditor",
|
||||
"win32x64AppId": "{{9D394D01-1728-45A7-B997-A6C82C5452C3}",
|
||||
"win32arm64AppId": "{{0668DD58-2BDE-4101-8CDA-40252DF8875D}",
|
||||
"win32x64UserAppId": "{{8BED5DC1-6C55-46E6-9FE6-18F7E6F7C7F1}",
|
||||
"win32arm64UserAppId": "{{F6C87466-BC82-4A8F-B0FF-18CA366BA4D8}",
|
||||
"win32AppUserModelId": "Void.Editor",
|
||||
"win32ShellNameShort": "V&oid",
|
||||
"win32TunnelServiceMutex": "void-tunnelservice",
|
||||
"win32TunnelMutex": "void-tunnel",
|
||||
"darwinBundleIdentifier": "com.voideditor.code",
|
||||
"linuxIconName": "void-editor",
|
||||
"licenseFileName": "LICENSE.txt",
|
||||
"reportIssueUrl": "https://github.com/microsoft/vscode/issues/new",
|
||||
"reportIssueUrl": "https://github.com/voideditor/void/issues/new",
|
||||
"nodejsRepository": "https://nodejs.org",
|
||||
"urlProtocol": "code-oss",
|
||||
"webviewContentExternalBaseUrlTemplate": "https://{{uuid}}.vscode-cdn.net/insider/ef65ac1ba57f57f2a3961bfe94aa20481caca4c6/out/vs/workbench/contrib/webview/browser/pre/",
|
||||
"builtInExtensions": [
|
||||
{
|
||||
"name": "ms-vscode.js-debug-companion",
|
||||
"version": "1.1.3",
|
||||
"sha256": "7380a890787452f14b2db7835dfa94de538caf358ebc263f9d46dd68ac52de93",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug-companion",
|
||||
"metadata": {
|
||||
"id": "99cb0b7f-7354-4278-b8da-6cc79972169d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.js-debug",
|
||||
"version": "1.93.0",
|
||||
"sha256": "9339cb8e6b77f554df54d79e71f533279cb76b0f9b04c207f633bfd507442b6a",
|
||||
"repo": "https://github.com/microsoft/vscode-js-debug",
|
||||
"metadata": {
|
||||
"id": "25629058-ddac-4e17-abba-74678e126c5d",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
},
|
||||
{
|
||||
"name": "ms-vscode.vscode-js-profile-table",
|
||||
"version": "1.0.9",
|
||||
"sha256": "3b62ee4276a2bbea3fe230f94b1d5edd915b05966090ea56f882e1e0ab53e1a6",
|
||||
"repo": "https://github.com/microsoft/vscode-js-profile-visualizer",
|
||||
"metadata": {
|
||||
"id": "7e52b41b-71ad-457b-ab7e-0620f1fc4feb",
|
||||
"publisherId": {
|
||||
"publisherId": "5f5636e7-69ed-4afe-b5d6-8d231fb3d3ee",
|
||||
"publisherName": "ms-vscode",
|
||||
"displayName": "Microsoft",
|
||||
"flags": "verified"
|
||||
},
|
||||
"publisherDisplayName": "Microsoft"
|
||||
}
|
||||
}
|
||||
]
|
||||
"urlProtocol": "void-editor",
|
||||
"extensionsGallery": {
|
||||
"serviceUrl": "https://open-vsx.org/vscode/gallery",
|
||||
"itemUrl": "https://open-vsx.org/vscode/item"
|
||||
},
|
||||
"builtInExtensions": []
|
||||
}
|
||||
|
||||
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 813 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 2.7 KiB After Width: | Height: | Size: 254 KiB |
|
Before Width: | Height: | Size: 34 KiB After Width: | Height: | Size: 29 KiB |
@@ -1,9 +1,9 @@
|
||||
<Application xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance">
|
||||
<VisualElements
|
||||
BackgroundColor="#2D2D30"
|
||||
BackgroundColor="#FFFFFF"
|
||||
ShowNameOnSquare150x150Logo="on"
|
||||
Square150x150Logo="resources\app\resources\win32\code_150x150.png"
|
||||
Square70x70Logo="resources\app\resources\win32\code_70x70.png"
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Code - OSS" />
|
||||
ForegroundText="light"
|
||||
ShortDisplayName="Void" />
|
||||
</Application>
|
||||
|
||||
|
Before Width: | Height: | Size: 89 KiB After Width: | Height: | Size: 29 KiB |
|
Before Width: | Height: | Size: 395 B After Width: | Height: | Size: 5.9 KiB |
|
Before Width: | Height: | Size: 338 B After Width: | Height: | Size: 1.8 KiB |
|
After Width: | Height: | Size: 1.4 MiB |
|
After Width: | Height: | Size: 795 KiB |
@@ -123,7 +123,7 @@ protocol.registerSchemesAsPrivileged([
|
||||
},
|
||||
{
|
||||
scheme: 'vscode-file',
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true }
|
||||
privileges: { secure: true, standard: true, supportFetchAPI: true, corsEnabled: true, codeCache: true, }
|
||||
}
|
||||
]);
|
||||
|
||||
|
||||
@@ -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",
|
||||
]
|
||||
}
|
||||
|
||||
@@ -159,7 +159,7 @@ export class InputBox extends Widget {
|
||||
this.scrollableElement = new ScrollableElement(this.element, { vertical: ScrollbarVisibility.Auto });
|
||||
|
||||
if (this.options.flexibleWidth) {
|
||||
this.input.setAttribute('wrap', 'off');
|
||||
this.input.setAttribute('wrap', 'on');
|
||||
this.mirror.style.whiteSpace = 'pre';
|
||||
this.mirror.style.wordWrap = 'initial';
|
||||
}
|
||||
|
||||
@@ -121,6 +121,11 @@ import { normalizeNFC } from '../../base/common/normalization.js';
|
||||
import { ICSSDevelopmentService, CSSDevelopmentService } from '../../platform/cssDev/node/cssDevService.js';
|
||||
import { ExtensionSignatureVerificationService, IExtensionSignatureVerificationService } from '../../platform/extensionManagement/node/extensionSignatureVerificationService.js';
|
||||
|
||||
import { LLMMessageChannel } from '../../platform/void/electron-main/llmMessageChannel.js';
|
||||
import { IMetricsService } from '../../platform/void/common/metricsService.js';
|
||||
import { MetricsMainService } from '../../platform/void/electron-main/metricsMainService.js';
|
||||
import { VoidMainUpdateService } from '../../platform/void/electron-main/voidUpdateMainService.js';
|
||||
import { IVoidUpdateService } from '../../platform/void/common/voidUpdateService.js';
|
||||
/**
|
||||
* 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 +153,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 +513,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 +1106,10 @@ 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));
|
||||
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
|
||||
|
||||
// Default Extensions Profile Init
|
||||
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
|
||||
services.set(IExtensionsScannerService, new SyncDescriptor(ExtensionsScannerService, undefined, true));
|
||||
@@ -1225,6 +1244,16 @@ export class CodeApplication extends Disposable {
|
||||
mainProcessElectronServer.registerChannel('logger', loggerChannel);
|
||||
sharedProcessClient.then(client => client.registerChannel('logger', loggerChannel));
|
||||
|
||||
// Void - use loggerChannel as reference
|
||||
const metricsChannel = ProxyChannel.fromService(accessor.get(IMetricsService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-metrics', metricsChannel);
|
||||
|
||||
const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables);
|
||||
mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel);
|
||||
|
||||
const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
|
||||
mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel);
|
||||
|
||||
// Extension Host Debug Broadcasting
|
||||
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
|
||||
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
|
||||
|
||||
@@ -3,6 +3,7 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { ICodeEditor, IDiffEditor } from '../editorBrowser.js';
|
||||
import { IDecorationRenderOptions } from '../../common/editorCommon.js';
|
||||
|
||||
@@ -0,0 +1,135 @@
|
||||
import { Disposable } from '../../../../base/common/lifecycle.js';
|
||||
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
|
||||
import { IModelDeltaDecoration } from '../../../common/model.js';
|
||||
import { ICodeEditor, IViewZone } from '../../editorBrowser.js';
|
||||
import { IRange } from '../../../common/core/range.js';
|
||||
import { EditorOption } from '../../../common/config/editorOptions.js';
|
||||
|
||||
|
||||
// THIS FILE IS OLD + UNUSED!!!
|
||||
|
||||
// SEE inlineDiffsService.ts INSTEAD.
|
||||
|
||||
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;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
@@ -223,6 +223,11 @@ export class BaseEditorSimpleWorker implements IDisposable, IWorkerTextModelSync
|
||||
private static readonly _diffLimit = 100000;
|
||||
|
||||
public async $computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): Promise<TextEdit[]> {
|
||||
return this.$Void_computeMoreMinimalEdits(modelUrl, edits, pretty)
|
||||
}
|
||||
|
||||
// Void added this as non async
|
||||
public $Void_computeMoreMinimalEdits(modelUrl: string, edits: TextEdit[], pretty: boolean): TextEdit[] {
|
||||
const model = this._getModel(modelUrl);
|
||||
if (!model) {
|
||||
return edits;
|
||||
|
||||
@@ -22,7 +22,7 @@ export class ExpandLineSelectionAction extends EditorAction {
|
||||
kbOpts: {
|
||||
weight: KeybindingWeight.EditorCore,
|
||||
kbExpr: EditorContextKeys.textInputFocus,
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyL
|
||||
primary: KeyMod.CtrlCmd | KeyCode.KeyM // Void changed this to Cmd+M
|
||||
},
|
||||
});
|
||||
}
|
||||
|
||||
@@ -403,6 +403,7 @@ function isVersionValid(currentVersion: string, date: ProductDate, requestedVers
|
||||
}
|
||||
|
||||
if (!isValidVersion(currentVersion, date, desiredVersion)) {
|
||||
// Void - ignore not compatible
|
||||
notices.push(nls.localize('versionMismatch', "Extension is not compatible with Code {0}. Extension requires: {1}.", currentVersion, requestedVersion));
|
||||
return false;
|
||||
}
|
||||
|
||||
@@ -64,7 +64,8 @@ export const enum KeybindingWeight {
|
||||
EditorContrib = 100,
|
||||
WorkbenchContrib = 200,
|
||||
BuiltinExtension = 300,
|
||||
ExternalExtension = 400
|
||||
ExternalExtension = 400,
|
||||
VoidExtension = 605, // Void - must trump any external extension
|
||||
}
|
||||
|
||||
export interface ICommandAndKeybindingRule extends IKeybindingRule {
|
||||
|
||||
@@ -31,25 +31,29 @@ export class ServerTelemetryService extends TelemetryService implements IServerT
|
||||
}
|
||||
|
||||
override publicLog(eventName: string, data?: ITelemetryData) {
|
||||
if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
|
||||
return;
|
||||
}
|
||||
return super.publicLog(eventName, data);
|
||||
// Void commented this out
|
||||
// if (this._injectedTelemetryLevel < TelemetryLevel.USAGE) {
|
||||
// return;
|
||||
// }
|
||||
// return super.publicLog(eventName, data);
|
||||
}
|
||||
|
||||
override publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
return this.publicLog(eventName, data as ITelemetryData | undefined);
|
||||
// Void commented this out
|
||||
// return this.publicLog(eventName, data as ITelemetryData | undefined);
|
||||
}
|
||||
|
||||
override publicLogError(errorEventName: string, data?: ITelemetryData) {
|
||||
if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
|
||||
return Promise.resolve(undefined);
|
||||
}
|
||||
return super.publicLogError(errorEventName, data);
|
||||
// Void commented this out
|
||||
// if (this._injectedTelemetryLevel < TelemetryLevel.ERROR) {
|
||||
// return Promise.resolve(undefined);
|
||||
// }
|
||||
// return super.publicLogError(errorEventName, data);
|
||||
}
|
||||
|
||||
override publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
return this.publicLogError(eventName, data as ITelemetryData | undefined);
|
||||
// Void commented this out
|
||||
// return this.publicLogError(eventName, data as ITelemetryData | undefined);
|
||||
}
|
||||
|
||||
async updateInjectedTelemetryLevel(telemetryLevel: TelemetryLevel): Promise<void> {
|
||||
|
||||
@@ -4,7 +4,7 @@
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { DisposableStore } from '../../../base/common/lifecycle.js';
|
||||
import { mixin } from '../../../base/common/objects.js';
|
||||
// import { mixin } from '../../../base/common/objects.js';
|
||||
import { isWeb } from '../../../base/common/platform.js';
|
||||
import { escapeRegExpCharacters } from '../../../base/common/strings.js';
|
||||
import { localize } from '../../../nls.js';
|
||||
@@ -15,7 +15,8 @@ import { IProductService } from '../../product/common/productService.js';
|
||||
import { Registry } from '../../registry/common/platform.js';
|
||||
import { ClassifiedEvent, IGDPRProperty, OmitMetadata, StrictPropertyCheck } from './gdprTypings.js';
|
||||
import { ITelemetryData, ITelemetryService, TelemetryConfiguration, TelemetryLevel, TELEMETRY_CRASH_REPORTER_SETTING_ID, TELEMETRY_OLD_SETTING_ID, TELEMETRY_SECTION_ID, TELEMETRY_SETTING_ID, ICommonProperties } from './telemetry.js';
|
||||
import { cleanData, getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
|
||||
import { getTelemetryLevel, ITelemetryAppender } from './telemetryUtils.js';
|
||||
// import { cleanData } from './telemetryUtils.js';
|
||||
|
||||
export interface ITelemetryServiceConfig {
|
||||
appenders: ITelemetryAppender[];
|
||||
@@ -38,7 +39,7 @@ export class TelemetryService implements ITelemetryService {
|
||||
readonly firstSessionDate: string;
|
||||
readonly msftInternal: boolean | undefined;
|
||||
|
||||
private _appenders: ITelemetryAppender[];
|
||||
// private _appenders: ITelemetryAppender[];
|
||||
private _commonProperties: ICommonProperties;
|
||||
private _experimentProperties: { [name: string]: string } = {};
|
||||
private _piiPaths: string[];
|
||||
@@ -53,7 +54,7 @@ export class TelemetryService implements ITelemetryService {
|
||||
@IConfigurationService private _configurationService: IConfigurationService,
|
||||
@IProductService private _productService: IProductService
|
||||
) {
|
||||
this._appenders = config.appenders;
|
||||
// this._appenders = config.appenders;
|
||||
this._commonProperties = config.commonProperties ?? Object.create(null);
|
||||
|
||||
this.sessionId = this._commonProperties['sessionID'] as string;
|
||||
@@ -121,44 +122,47 @@ export class TelemetryService implements ITelemetryService {
|
||||
this._disposables.dispose();
|
||||
}
|
||||
|
||||
private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
|
||||
// don't send events when the user is optout
|
||||
if (this._telemetryLevel < eventLevel) {
|
||||
return;
|
||||
}
|
||||
// Void commented this out
|
||||
// private _log(eventName: string, eventLevel: TelemetryLevel, data?: ITelemetryData) {
|
||||
// // don't send events when the user is optout
|
||||
// if (this._telemetryLevel < eventLevel) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// add experiment properties
|
||||
data = mixin(data, this._experimentProperties);
|
||||
// // add experiment properties
|
||||
// data = mixin(data, this._experimentProperties);
|
||||
|
||||
// remove all PII from data
|
||||
data = cleanData(data as Record<string, any>, this._cleanupPatterns);
|
||||
// // remove all PII from data
|
||||
// data = cleanData(data as Record<string, any>, this._cleanupPatterns);
|
||||
|
||||
// add common properties
|
||||
data = mixin(data, this._commonProperties);
|
||||
// // add common properties
|
||||
// data = mixin(data, this._commonProperties);
|
||||
|
||||
// Log to the appenders of sufficient level
|
||||
this._appenders.forEach(a => a.log(eventName, data));
|
||||
}
|
||||
// // Log to the appenders of sufficient level
|
||||
// this._appenders.forEach(a => a.log(eventName, data));
|
||||
// }
|
||||
|
||||
publicLog(eventName: string, data?: ITelemetryData) {
|
||||
this._log(eventName, TelemetryLevel.USAGE, data);
|
||||
// this._log(eventName, TelemetryLevel.USAGE, data);
|
||||
}
|
||||
|
||||
publicLog2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
this.publicLog(eventName, data as ITelemetryData);
|
||||
// this.publicLog(eventName, data as ITelemetryData);
|
||||
}
|
||||
|
||||
publicLogError(errorEventName: string, data?: ITelemetryData) {
|
||||
if (!this._sendErrorTelemetry) {
|
||||
return;
|
||||
}
|
||||
// Void commented this out
|
||||
// if (!this._sendErrorTelemetry) {
|
||||
// return;
|
||||
// }
|
||||
|
||||
// Send error event and anonymize paths
|
||||
this._log(errorEventName, TelemetryLevel.ERROR, data);
|
||||
// // Send error event and anonymize paths
|
||||
// this._log(errorEventName, TelemetryLevel.ERROR, data);
|
||||
}
|
||||
|
||||
publicLogError2<E extends ClassifiedEvent<OmitMetadata<T>> = never, T extends IGDPRProperty = never>(eventName: string, data?: StrictPropertyCheck<T, E>) {
|
||||
this.publicLogError(eventName, data as ITelemetryData);
|
||||
// Void commented this out
|
||||
// this.publicLogError(eventName, data as ITelemetryData);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -3,11 +3,12 @@
|
||||
* Licensed under the MIT License. See License.txt in the project root for license information.
|
||||
*--------------------------------------------------------------------------------------------*/
|
||||
|
||||
import { timeout } from '../../../base/common/async.js';
|
||||
// import { timeout } from '../../../base/common/async.js';
|
||||
import { CancellationToken } from '../../../base/common/cancellation.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { IConfigurationService } from '../../configuration/common/configuration.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
// import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/electron-main/lifecycleMainService.js';
|
||||
import { ILogService } from '../../log/common/log.js';
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
@@ -15,7 +16,10 @@ import { IRequestService } from '../../request/common/request.js';
|
||||
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
|
||||
|
||||
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
|
||||
return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// return `https://voideditor.dev/api/update/${platform}/stable`;
|
||||
// return `${productService.updateUrl}/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
// https://github.com/VSCodium/update-api
|
||||
return `https://updates.voideditor.dev/api/update/${platform}/${quality}/${productService.commit}`;
|
||||
}
|
||||
|
||||
export type UpdateNotAvailableClassification = {
|
||||
@@ -57,7 +61,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
@IEnvironmentMainService private readonly environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService protected requestService: IRequestService,
|
||||
@ILogService protected logService: ILogService,
|
||||
@IProductService protected readonly productService: IProductService
|
||||
@IProductService protected readonly productService: IProductService,
|
||||
) {
|
||||
lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen)
|
||||
.finally(() => this.initialize());
|
||||
@@ -70,75 +74,35 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
*/
|
||||
protected async initialize(): Promise<void> {
|
||||
if (!this.environmentMainService.isBuilt) {
|
||||
console.log('is NOT built, canceling update service')
|
||||
this.setState(State.Disabled(DisablementReason.NotBuilt));
|
||||
return; // updates are never enabled when running out of sources
|
||||
}
|
||||
console.log('is built, continuing with update service')
|
||||
|
||||
if (this.environmentMainService.disableUpdates) {
|
||||
this.setState(State.Disabled(DisablementReason.DisabledByEnvironment));
|
||||
this.logService.info('update#ctor - updates are disabled by the environment');
|
||||
return;
|
||||
}
|
||||
|
||||
if (!this.productService.updateUrl || !this.productService.commit) {
|
||||
this.setState(State.Disabled(DisablementReason.MissingConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as there is no update URL');
|
||||
return;
|
||||
}
|
||||
|
||||
const updateMode = this.configurationService.getValue<'none' | 'manual' | 'start' | 'default'>('update.mode');
|
||||
const quality = this.getProductQuality(updateMode);
|
||||
|
||||
if (!quality) {
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
this.logService.info('update#ctor - updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
|
||||
this.url = this.buildUpdateFeedUrl(quality);
|
||||
this.url = this.doBuildUpdateFeedUrl('stable');
|
||||
if (!this.url) {
|
||||
this.setState(State.Disabled(DisablementReason.InvalidConfiguration));
|
||||
this.logService.info('update#ctor - updates are disabled as the update URL is badly formed');
|
||||
return;
|
||||
}
|
||||
|
||||
// hidden setting
|
||||
if (this.configurationService.getValue<boolean>('_update.prss')) {
|
||||
const url = new URL(this.url);
|
||||
url.searchParams.set('prss', 'true');
|
||||
this.url = url.toString();
|
||||
}
|
||||
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
|
||||
|
||||
this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
if (updateMode === 'manual') {
|
||||
this.logService.info('update#ctor - manual checks only; automatic updates are disabled by user preference');
|
||||
return;
|
||||
}
|
||||
// Void - temporarily disabled while we figure out how to do this the right way
|
||||
|
||||
if (updateMode === 'start') {
|
||||
this.logService.info('update#ctor - startup checks only; automatic updates are disabled by user preference');
|
||||
// this.setState(State.Idle(this.getUpdateType()));
|
||||
|
||||
// Check for updates only once after 30 seconds
|
||||
setTimeout(() => this.checkForUpdates(false), 30 * 1000);
|
||||
} else {
|
||||
// Start checking for updates after 30 seconds
|
||||
this.scheduleCheckForUpdates(30 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
// start checking for updates after 10 seconds
|
||||
// this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
|
||||
}
|
||||
|
||||
private getProductQuality(updateMode: string): string | undefined {
|
||||
return updateMode === 'none' ? undefined : this.productService.quality;
|
||||
}
|
||||
|
||||
private scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
return timeout(delay)
|
||||
.then(() => this.checkForUpdates(false))
|
||||
.then(() => {
|
||||
// Check again after 1 hour
|
||||
return this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
});
|
||||
}
|
||||
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
|
||||
// await timeout(delay);
|
||||
// await this.checkForUpdates(false);
|
||||
// return await this.scheduleCheckForUpdates(60 * 60 * 1000);
|
||||
// }
|
||||
|
||||
async checkForUpdates(explicit: boolean): Promise<void> {
|
||||
this.logService.trace('update#checkForUpdates, state = ', this.state.type);
|
||||
@@ -160,6 +124,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
await this.doDownloadUpdate(this.state);
|
||||
}
|
||||
|
||||
// override implemented by windows and linux
|
||||
protected async doDownloadUpdate(state: AvailableForDownload): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
@@ -174,6 +139,7 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
await this.doApplyUpdate();
|
||||
}
|
||||
|
||||
// windows overrides this
|
||||
protected async doApplyUpdate(): Promise<void> {
|
||||
// noop
|
||||
}
|
||||
@@ -236,6 +202,6 @@ export abstract class AbstractUpdateService implements IUpdateService {
|
||||
// noop
|
||||
}
|
||||
|
||||
protected abstract buildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doBuildUpdateFeedUrl(quality: string): string | undefined;
|
||||
protected abstract doCheckForUpdates(context: any): void;
|
||||
}
|
||||
|
||||
@@ -34,7 +34,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
|
||||
@IRequestService requestService: IRequestService,
|
||||
@ILogService logService: ILogService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
@@ -73,7 +73,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
|
||||
this.setState(State.Idle(UpdateType.Archive, message));
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string | undefined {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let assetID: string;
|
||||
if (!this.productService.darwinUniversalAssetId) {
|
||||
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
|
||||
|
||||
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string {
|
||||
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
|
||||
}
|
||||
|
||||
|
||||
@@ -67,7 +67,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
@ILogService logService: ILogService,
|
||||
@IFileService private readonly fileService: IFileService,
|
||||
@INativeHostMainService private readonly nativeHostMainService: INativeHostMainService,
|
||||
@IProductService productService: IProductService
|
||||
@IProductService productService: IProductService,
|
||||
) {
|
||||
super(lifecycleMainService, configurationService, environmentMainService, requestService, logService, productService);
|
||||
|
||||
@@ -99,7 +99,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
|
||||
await super.initialize();
|
||||
}
|
||||
|
||||
protected buildUpdateFeedUrl(quality: string): string | undefined {
|
||||
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
|
||||
let platform = `win32-${process.arch}`;
|
||||
|
||||
if (getUpdateType() === UpdateType.Archive) {
|
||||
|
||||
@@ -0,0 +1,21 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// ---------- common ----------
|
||||
|
||||
// llmMessage
|
||||
import '../common/llmMessageService.js'
|
||||
|
||||
// voidSettings
|
||||
import '../common/voidSettingsService.js'
|
||||
|
||||
// refreshModel
|
||||
import '../common/refreshModelService.js'
|
||||
|
||||
// metrics
|
||||
import '../common/metricsService.js'
|
||||
|
||||
// updates
|
||||
import '../common/voidUpdateService.js'
|
||||
@@ -0,0 +1,179 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from './llmMessageTypes.js';
|
||||
import { IChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
// import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
// calls channel to implement features
|
||||
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
|
||||
|
||||
export interface ILLMMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
sendLLMMessage: (params: ServiceSendLLMMessageParams) => string | null;
|
||||
abort: (requestId: string) => void;
|
||||
ollamaList: (params: ServiceModelListParams<OllamaModelResponse>) => void;
|
||||
openAICompatibleList: (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => void;
|
||||
}
|
||||
|
||||
export class LLMMessageService extends Disposable implements ILLMMessageService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly channel: IChannel // LLMMessageChannel
|
||||
|
||||
// llmMessage
|
||||
private readonly onTextHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) } = {}
|
||||
private readonly onFinalMessageHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) } = {}
|
||||
private readonly onErrorHooks_llm: { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) } = {}
|
||||
|
||||
|
||||
// ollamaList
|
||||
private readonly onSuccess_ollama: { [eventId: string]: ((params: EventModelListOnSuccessParams<OllamaModelResponse>) => void) } = {}
|
||||
private readonly onError_ollama: { [eventId: string]: ((params: EventModelListOnErrorParams<OllamaModelResponse>) => void) } = {}
|
||||
|
||||
// openAICompatibleList
|
||||
private readonly onSuccess_openAICompatible: { [eventId: string]: ((params: EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
private readonly onError_openAICompatible: { [eventId: string]: ((params: EventModelListOnErrorParams<OpenaiCompatibleModelResponse>) => void) } = {}
|
||||
|
||||
constructor(
|
||||
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
// @INotificationService private readonly notificationService: INotificationService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// const service = ProxyChannel.toService<LLMMessageChannel>(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service
|
||||
// see llmMessageChannel.ts
|
||||
this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService')
|
||||
|
||||
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
|
||||
// llm
|
||||
this._register((this.channel.listen('onText_llm') satisfies Event<EventLLMMessageOnTextParams>)(e => {
|
||||
this.onTextHooks_llm[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onFinalMessage_llm') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => {
|
||||
this.onFinalMessageHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_llm') satisfies Event<EventLLMMessageOnErrorParams>)(e => {
|
||||
console.error('Error in LLMMessageService:', JSON.stringify(e))
|
||||
this.onErrorHooks_llm[e.requestId]?.(e)
|
||||
this._onRequestIdDone(e.requestId)
|
||||
}))
|
||||
// ollama .list()
|
||||
this._register((this.channel.listen('onSuccess_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => {
|
||||
this.onSuccess_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => {
|
||||
this.onError_ollama[e.requestId]?.(e)
|
||||
}))
|
||||
// openaiCompatible .list()
|
||||
this._register((this.channel.listen('onSuccess_openAICompatible') satisfies Event<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onSuccess_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
this._register((this.channel.listen('onError_openAICompatible') satisfies Event<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>)(e => {
|
||||
this.onError_openAICompatible[e.requestId]?.(e)
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
sendLLMMessage(params: ServiceSendLLMMessageParams) {
|
||||
const { onText, onFinalMessage, onError, ...proxyParams } = params;
|
||||
const { useProviderFor: featureName } = proxyParams
|
||||
|
||||
// end early if no provider
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
|
||||
if (modelSelection === null) {
|
||||
onError({ message: 'Please add a Provider in Settings!', fullError: null })
|
||||
return null
|
||||
}
|
||||
const { providerName, modelName } = modelSelection
|
||||
|
||||
const aiInstructions = this.voidSettingsService.state.globalSettings.aiInstructions
|
||||
if (aiInstructions)
|
||||
proxyParams.messages.unshift({ role: 'system', content: aiInstructions })
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onTextHooks_llm[requestId_] = onText
|
||||
this.onFinalMessageHooks_llm[requestId_] = onFinalMessage
|
||||
this.onErrorHooks_llm[requestId_] = onError
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// params will be stripped of all its functions over the IPC channel
|
||||
this.channel.call('sendLLMMessage', {
|
||||
...proxyParams,
|
||||
requestId: requestId_,
|
||||
providerName,
|
||||
modelName,
|
||||
settingsOfProvider,
|
||||
} satisfies MainLLMMessageParams);
|
||||
|
||||
return requestId_
|
||||
}
|
||||
|
||||
|
||||
abort(requestId: string) {
|
||||
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
|
||||
this._onRequestIdDone(requestId)
|
||||
}
|
||||
|
||||
|
||||
ollamaList = (params: ServiceModelListParams<OllamaModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_ollama[requestId_] = onSuccess
|
||||
this.onError_ollama[requestId_] = onError
|
||||
|
||||
this.channel.call('ollamaList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OllamaModelResponse>)
|
||||
}
|
||||
|
||||
openAICompatibleList = (params: ServiceModelListParams<OpenaiCompatibleModelResponse>) => {
|
||||
const { onSuccess, onError, ...proxyParams } = params
|
||||
|
||||
const { settingsOfProvider } = this.voidSettingsService.state
|
||||
|
||||
// add state for request id
|
||||
const requestId_ = generateUuid();
|
||||
this.onSuccess_openAICompatible[requestId_] = onSuccess
|
||||
this.onError_openAICompatible[requestId_] = onError
|
||||
|
||||
this.channel.call('openAICompatibleList', {
|
||||
...proxyParams,
|
||||
settingsOfProvider,
|
||||
requestId: requestId_,
|
||||
} satisfies MainModelListParams<OpenaiCompatibleModelResponse>)
|
||||
}
|
||||
|
||||
|
||||
|
||||
_onRequestIdDone(requestId: string) {
|
||||
delete this.onTextHooks_llm[requestId]
|
||||
delete this.onFinalMessageHooks_llm[requestId]
|
||||
delete this.onErrorHooks_llm[requestId]
|
||||
|
||||
delete this.onSuccess_ollama[requestId]
|
||||
delete this.onError_ollama[requestId]
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(ILLMMessageService, LLMMessageService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,162 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { IRange } from '../../../editor/common/core/range'
|
||||
import { ProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
|
||||
|
||||
|
||||
export const errorDetails = (fullError: Error | null): string | null => {
|
||||
if (fullError === null) {
|
||||
return null
|
||||
}
|
||||
else if (typeof fullError === 'object') {
|
||||
return JSON.stringify(fullError, null, 2)
|
||||
}
|
||||
else if (typeof fullError === 'string') {
|
||||
return null
|
||||
}
|
||||
return null
|
||||
}
|
||||
|
||||
export type OnText = (p: { newText: string, fullText: string }) => void
|
||||
export type OnFinalMessage = (p: { fullText: string }) => void
|
||||
export type OnError = (p: { message: string, fullError: Error | null }) => void
|
||||
export type AbortRef = { current: (() => void) | null }
|
||||
|
||||
export type LLMMessage = {
|
||||
role: 'system' | 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
|
||||
export type ServiceSendLLMFeatureParams = {
|
||||
useProviderFor: 'Ctrl+K';
|
||||
range: IRange;
|
||||
} | {
|
||||
useProviderFor: 'Ctrl+L';
|
||||
} | {
|
||||
useProviderFor: 'Autocomplete';
|
||||
range: IRange;
|
||||
}
|
||||
|
||||
// params to the true sendLLMMessage function
|
||||
export type LLMMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
abortRef: AbortRef;
|
||||
|
||||
messages: LLMMessage[];
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
providerName: ProviderName;
|
||||
modelName: string;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
}
|
||||
|
||||
export type ServiceSendLLMMessageParams = {
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
|
||||
messages: LLMMessage[];
|
||||
|
||||
logging: {
|
||||
loggingName: string,
|
||||
};
|
||||
} & ServiceSendLLMFeatureParams
|
||||
|
||||
// can't send functions across a proxy, use listeners instead
|
||||
export type BlockedMainLLMMessageParams = 'onText' | 'onFinalMessage' | 'onError' | 'abortRef'
|
||||
|
||||
export type MainLLMMessageParams = Omit<LLMMMessageParams, BlockedMainLLMMessageParams> & { requestId: string }
|
||||
export type MainLLMMessageAbortParams = { requestId: string }
|
||||
|
||||
export type EventLLMMessageOnTextParams = Parameters<OnText>[0] & { requestId: string }
|
||||
export type EventLLMMessageOnFinalMessageParams = Parameters<OnFinalMessage>[0] & { requestId: string }
|
||||
export type EventLLMMessageOnErrorParams = Parameters<OnError>[0] & { requestId: string }
|
||||
|
||||
export type _InternalSendLLMMessageFnType = (params: {
|
||||
messages: LLMMessage[];
|
||||
onText: OnText;
|
||||
onFinalMessage: OnFinalMessage;
|
||||
onError: OnError;
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
providerName: ProviderName;
|
||||
modelName: string;
|
||||
|
||||
_setAborter: (aborter: () => void) => void;
|
||||
}) => void
|
||||
|
||||
// service -> main -> internal -> event (back to main)
|
||||
// (browser)
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// These are from 'ollama' SDK
|
||||
interface OllamaModelDetails {
|
||||
parent_model: string;
|
||||
format: string;
|
||||
family: string;
|
||||
families: string[];
|
||||
parameter_size: string;
|
||||
quantization_level: string;
|
||||
}
|
||||
|
||||
export type OllamaModelResponse = {
|
||||
name: string;
|
||||
modified_at: Date;
|
||||
size: number;
|
||||
digest: string;
|
||||
details: OllamaModelDetails;
|
||||
expires_at: Date;
|
||||
size_vram: number;
|
||||
}
|
||||
|
||||
export type OpenaiCompatibleModelResponse = {
|
||||
id: string;
|
||||
created: number;
|
||||
object: 'model';
|
||||
owned_by: string;
|
||||
}
|
||||
|
||||
|
||||
// params to the true list fn
|
||||
export type ModelListParams<modelResponse> = {
|
||||
settingsOfProvider: SettingsOfProvider;
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: string }) => void;
|
||||
}
|
||||
|
||||
// params to the service
|
||||
export type ServiceModelListParams<modelResponse> = {
|
||||
onSuccess: (param: { models: modelResponse[] }) => void;
|
||||
onError: (param: { error: any }) => void;
|
||||
}
|
||||
|
||||
type BlockedMainModelListParams = 'onSuccess' | 'onError'
|
||||
export type MainModelListParams<modelResponse> = Omit<ModelListParams<modelResponse>, BlockedMainModelListParams> & { requestId: string }
|
||||
|
||||
export type EventModelListOnSuccessParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onSuccess']>[0] & { requestId: string }
|
||||
export type EventModelListOnErrorParams<modelResponse> = Parameters<ModelListParams<modelResponse>['onError']>[0] & { requestId: string }
|
||||
|
||||
|
||||
|
||||
|
||||
export type _InternalModelListFnType<modelResponse> = (params: ModelListParams<modelResponse>) => void
|
||||
@@ -0,0 +1,68 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { Action2, registerAction2 } from '../../actions/common/actions.js';
|
||||
import { localize2 } from '../../../nls.js';
|
||||
import { ServicesAccessor } from '../../../editor/browser/editorExtensions.js';
|
||||
import { INotificationService } from '../../notification/common/notification.js';
|
||||
|
||||
export interface IMetricsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
capture(event: string, params: Record<string, any>): void;
|
||||
getDebuggingProperties(): Promise<object>;
|
||||
}
|
||||
|
||||
export const IMetricsService = createDecorator<IMetricsService>('metricsService');
|
||||
|
||||
|
||||
// implemented by calling channel
|
||||
export class MetricsService implements IMetricsService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly metricsService: IMetricsService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService // (only usable on client side)
|
||||
) {
|
||||
// creates an IPC proxy to use metricsMainService.ts
|
||||
this.metricsService = ProxyChannel.toService<IMetricsService>(mainProcessService.getChannel('void-channel-metrics'));
|
||||
}
|
||||
|
||||
// call capture on the channel
|
||||
capture(...params: Parameters<IMetricsService['capture']>) {
|
||||
this.metricsService.capture(...params);
|
||||
}
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
async getDebuggingProperties(): Promise<object> {
|
||||
return this.metricsService.getDebuggingProperties()
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IMetricsService, MetricsService, InstantiationType.Eager);
|
||||
|
||||
|
||||
// debugging action
|
||||
registerAction2(class extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'voidDebugInfo',
|
||||
f1: true,
|
||||
title: localize2('voidMetricsDebug', 'Void: Log Debug Info'),
|
||||
});
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const metricsService = accessor.get(IMetricsService)
|
||||
const notifService = accessor.get(INotificationService)
|
||||
|
||||
const debugProperties = await metricsService.getDebuggingProperties()
|
||||
console.log('Metrics:', debugProperties)
|
||||
notifService.info(`Void Debug info:\n${JSON.stringify(debugProperties, null, 2)}`)
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,218 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
import { IVoidSettingsService } from './voidSettingsService.js';
|
||||
import { ILLMMessageService } from './llmMessageService.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable, IDisposable } from '../../../base/common/lifecycle.js';
|
||||
import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js';
|
||||
import { OllamaModelResponse, OpenaiCompatibleModelResponse } from './llmMessageTypes.js';
|
||||
|
||||
|
||||
|
||||
|
||||
type RefreshableState = ({
|
||||
state: 'init',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'refreshing',
|
||||
timeoutId: NodeJS.Timeout | null, // the timeoutId of the most recent call to refreshModels
|
||||
} | {
|
||||
state: 'finished',
|
||||
timeoutId: null,
|
||||
} | {
|
||||
state: 'error',
|
||||
timeoutId: null,
|
||||
})
|
||||
|
||||
|
||||
/*
|
||||
|
||||
user click -> error -> fire(error)
|
||||
\> success -> fire(success)
|
||||
finally: keep polling
|
||||
|
||||
poll -> do not fire
|
||||
|
||||
*/
|
||||
export type RefreshModelStateOfProvider = Record<RefreshableProviderName, RefreshableState>
|
||||
|
||||
|
||||
|
||||
const refreshBasedOn: { [k in RefreshableProviderName]: (keyof SettingsOfProvider[k])[] } = {
|
||||
ollama: ['_enabled', 'endpoint'],
|
||||
// openAICompatible: ['_enabled', 'endpoint', 'apiKey'],
|
||||
}
|
||||
const REFRESH_INTERVAL = 5_000
|
||||
// const COOLDOWN_TIMEOUT = 300
|
||||
|
||||
const autoOptions = { enableProviderOnSuccess: true, doNotFire: true }
|
||||
|
||||
// element-wise equals
|
||||
function eq<T>(a: T[], b: T[]): boolean {
|
||||
if (a.length !== b.length) return false
|
||||
for (let i = 0; i < a.length; i++) {
|
||||
if (a[i] !== b[i]) return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
export interface IRefreshModelService {
|
||||
readonly _serviceBrand: undefined;
|
||||
startRefreshingModels: (providerName: RefreshableProviderName, options: { enableProviderOnSuccess: boolean, doNotFire: boolean }) => void;
|
||||
onDidChangeState: Event<RefreshableProviderName>;
|
||||
state: RefreshModelStateOfProvider;
|
||||
}
|
||||
|
||||
export const IRefreshModelService = createDecorator<IRefreshModelService>('RefreshModelService');
|
||||
|
||||
export class RefreshModelService extends Disposable implements IRefreshModelService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<RefreshableProviderName>();
|
||||
readonly onDidChangeState: Event<RefreshableProviderName> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
|
||||
constructor(
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
|
||||
const disposables: Set<IDisposable> = new Set()
|
||||
|
||||
const initializeAutoPollingAndOnChange = () => {
|
||||
this._clearAllTimeouts()
|
||||
disposables.forEach(d => d.dispose())
|
||||
disposables.clear()
|
||||
|
||||
if (!voidSettingsService.state.globalSettings.autoRefreshModels) return
|
||||
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
|
||||
// const { _enabled: enabled } = this.voidSettingsService.state.settingsOfProvider[providerName]
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
|
||||
// every time providerName.enabled changes, refresh models too, like a useEffect
|
||||
let relevantVals = () => refreshBasedOn[providerName].map(settingName => voidSettingsService.state.settingsOfProvider[providerName][settingName])
|
||||
let prevVals = relevantVals() // each iteration of a for loop has its own context and vars, so this is ok
|
||||
disposables.add(
|
||||
voidSettingsService.onDidChangeState(() => { // we might want to debounce this
|
||||
const newVals = relevantVals()
|
||||
if (!eq(prevVals, newVals)) {
|
||||
|
||||
const prevEnabled = prevVals[0] as boolean
|
||||
const enabled = newVals[0] as boolean
|
||||
|
||||
// if it was just enabled, or there was a change and it wasn't to the enabled state, refresh
|
||||
if ((enabled && !prevEnabled) || (!enabled && !prevEnabled)) {
|
||||
// if user just clicked enable, refresh
|
||||
this.startRefreshingModels(providerName, autoOptions)
|
||||
}
|
||||
else {
|
||||
// else if user just clicked disable, don't refresh
|
||||
|
||||
// //give cooldown before re-enabling (or at least re-fetching)
|
||||
// const timeoutId = setTimeout(() => this.refreshModels(providerName, !enabled), COOLDOWN_TIMEOUT)
|
||||
// this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
prevVals = newVals
|
||||
}
|
||||
})
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// on mount (when get init settings state), and if a relevant feature flag changes, start refreshing models
|
||||
voidSettingsService.waitForInitState.then(() => {
|
||||
initializeAutoPollingAndOnChange()
|
||||
this._register(
|
||||
voidSettingsService.onDidChangeState((type) => { if (typeof type === 'object' && type[1] === 'autoRefreshModels') initializeAutoPollingAndOnChange() })
|
||||
)
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
state: RefreshModelStateOfProvider = {
|
||||
ollama: { state: 'init', timeoutId: null },
|
||||
}
|
||||
|
||||
|
||||
// start listening for models (and don't stop until success)
|
||||
startRefreshingModels: IRefreshModelService['startRefreshingModels'] = (providerName, options) => {
|
||||
|
||||
this._clearProviderTimeout(providerName)
|
||||
|
||||
this._setRefreshState(providerName, 'refreshing', options)
|
||||
|
||||
const autoPoll = () => {
|
||||
if (this.voidSettingsService.state.globalSettings.autoRefreshModels) {
|
||||
// resume auto-polling
|
||||
const timeoutId = setTimeout(() => this.startRefreshingModels(providerName, autoOptions), REFRESH_INTERVAL)
|
||||
this._setTimeoutId(providerName, timeoutId)
|
||||
}
|
||||
}
|
||||
const listFn = providerName === 'ollama' ? this.llmMessageService.ollamaList
|
||||
: providerName === 'openAICompatible' ? this.llmMessageService.openAICompatibleList
|
||||
: () => { }
|
||||
|
||||
listFn({
|
||||
onSuccess: ({ models }) => {
|
||||
|
||||
// set the models to the detected models
|
||||
this.voidSettingsService.setAutodetectedModels(
|
||||
providerName,
|
||||
models.map(model => {
|
||||
if (providerName === 'ollama') return (model as OllamaModelResponse).name;
|
||||
else if (providerName === 'openAICompatible') return (model as OpenaiCompatibleModelResponse).id;
|
||||
else throw new Error('refreshMode fn: unknown provider', providerName);
|
||||
}),
|
||||
{ enableProviderOnSuccess: options.enableProviderOnSuccess, hideRefresh: options.doNotFire }
|
||||
)
|
||||
|
||||
if (options.enableProviderOnSuccess) this.voidSettingsService.setSettingOfProvider(providerName, '_enabled', true)
|
||||
|
||||
this._setRefreshState(providerName, 'finished', options)
|
||||
autoPoll()
|
||||
},
|
||||
onError: ({ error }) => {
|
||||
this._setRefreshState(providerName, 'error', options)
|
||||
autoPoll()
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
}
|
||||
|
||||
_clearAllTimeouts() {
|
||||
for (const providerName of refreshableProviderNames) {
|
||||
this._clearProviderTimeout(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
_clearProviderTimeout(providerName: RefreshableProviderName) {
|
||||
// cancel any existing poll
|
||||
if (this.state[providerName].timeoutId) {
|
||||
clearTimeout(this.state[providerName].timeoutId)
|
||||
this._setTimeoutId(providerName, null)
|
||||
}
|
||||
}
|
||||
|
||||
private _setTimeoutId(providerName: RefreshableProviderName, timeoutId: NodeJS.Timeout | null) {
|
||||
this.state[providerName].timeoutId = timeoutId
|
||||
}
|
||||
|
||||
private _setRefreshState(providerName: RefreshableProviderName, state: RefreshableState['state'], options?: { doNotFire: boolean }) {
|
||||
if (options?.doNotFire) return
|
||||
this.state[providerName].state = state
|
||||
this._onDidChangeState.fire(providerName)
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IRefreshModelService, RefreshModelService, InstantiationType.Eager);
|
||||
|
||||
@@ -0,0 +1,297 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { deepClone } from '../../../base/common/objects.js';
|
||||
import { IEncryptionService } from '../../encryption/common/encryptionService.js';
|
||||
import { registerSingleton, InstantiationType } from '../../instantiation/common/extensions.js';
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { IStorageService, StorageScope, StorageTarget } from '../../storage/common/storage.js';
|
||||
import { IMetricsService } from './metricsService.js';
|
||||
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, modelInfoOfDefaultNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings } from './voidSettingsTypes.js';
|
||||
|
||||
|
||||
const STORAGE_KEY = 'void.settingsServiceStorage'
|
||||
|
||||
type SetSettingOfProviderFn = <S extends SettingName>(
|
||||
providerName: ProviderName,
|
||||
settingName: S,
|
||||
newVal: SettingsOfProvider[ProviderName][S extends keyof SettingsOfProvider[ProviderName] ? S : never],
|
||||
) => Promise<void>;
|
||||
|
||||
type SetModelSelectionOfFeatureFn = <K extends FeatureName>(
|
||||
featureName: K,
|
||||
newVal: ModelSelectionOfFeature[K],
|
||||
options?: { doNotApplyEffects?: true }
|
||||
) => Promise<void>;
|
||||
|
||||
type SetGlobalSettingFn = <T extends GlobalSettingName, >(settingName: T, newVal: GlobalSettings[T]) => void;
|
||||
|
||||
export type ModelOption = { name: string, selection: ModelSelection }
|
||||
|
||||
|
||||
|
||||
export type VoidSettingsState = {
|
||||
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
|
||||
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
|
||||
readonly globalSettings: GlobalSettings;
|
||||
|
||||
readonly _modelOptions: ModelOption[] // computed based on the two above items
|
||||
}
|
||||
|
||||
type RealVoidSettings = Exclude<keyof VoidSettingsState, '_modelOptions'>
|
||||
type EventProp<T extends RealVoidSettings = RealVoidSettings> = T extends 'globalSettings' ? [T, keyof VoidSettingsState[T]] : T | 'all'
|
||||
|
||||
|
||||
export interface IVoidSettingsService {
|
||||
readonly _serviceBrand: undefined;
|
||||
readonly state: VoidSettingsState; // in order to play nicely with react, you should immutably change state
|
||||
readonly waitForInitState: Promise<void>;
|
||||
|
||||
onDidChangeState: Event<EventProp>;
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn;
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
|
||||
setGlobalSetting: SetGlobalSettingFn;
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, modelNames: string[], logging: object): void;
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string): void;
|
||||
addModel(providerName: ProviderName, modelName: string): void;
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean;
|
||||
}
|
||||
|
||||
|
||||
let _computeModelOptions = (settingsOfProvider: SettingsOfProvider) => {
|
||||
let modelOptions: ModelOption[] = []
|
||||
for (const providerName of providerNames) {
|
||||
const providerConfig = settingsOfProvider[providerName]
|
||||
if (!providerConfig._enabled) continue // if disabled, don't display model options
|
||||
for (const { modelName, isHidden } of providerConfig.models) {
|
||||
if (isHidden) continue
|
||||
modelOptions.push({ name: `${modelName} (${providerName})`, selection: { providerName, modelName } })
|
||||
}
|
||||
}
|
||||
return modelOptions
|
||||
}
|
||||
|
||||
|
||||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
_modelOptions: _computeModelOptions(defaultSettingsOfProvider), // computed
|
||||
}
|
||||
return d
|
||||
}
|
||||
|
||||
|
||||
export const IVoidSettingsService = createDecorator<IVoidSettingsService>('VoidSettingsService');
|
||||
class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly _onDidChangeState = new Emitter<EventProp>();
|
||||
readonly onDidChangeState: Event<EventProp> = this._onDidChangeState.event; // this is primarily for use in react, so react can listen + update on state changes
|
||||
|
||||
state: VoidSettingsState;
|
||||
waitForInitState: Promise<void> // await this if you need a valid state initially
|
||||
|
||||
constructor(
|
||||
@IStorageService private readonly _storageService: IStorageService,
|
||||
@IEncryptionService private readonly _encryptionService: IEncryptionService,
|
||||
@IMetricsService private readonly _metricsService: IMetricsService,
|
||||
// could have used this, but it's clearer the way it is (+ slightly different eg StorageTarget.USER)
|
||||
// @ISecretStorageService private readonly _secretStorageService: ISecretStorageService,
|
||||
) {
|
||||
super()
|
||||
|
||||
// at the start, we haven't read the partial config yet, but we need to set state to something
|
||||
this.state = defaultState()
|
||||
|
||||
let resolver: () => void = () => { }
|
||||
this.waitForInitState = new Promise((res, rej) => resolver = res)
|
||||
|
||||
// read and update the actual state immediately
|
||||
this._readState().then(s => {
|
||||
this.state = s
|
||||
resolver()
|
||||
this._onDidChangeState.fire('all')
|
||||
})
|
||||
}
|
||||
|
||||
private async _readState(): Promise<VoidSettingsState> {
|
||||
const encryptedState = this._storageService.get(STORAGE_KEY, StorageScope.APPLICATION)
|
||||
|
||||
if (!encryptedState)
|
||||
return defaultState()
|
||||
|
||||
const stateStr = await this._encryptionService.decrypt(encryptedState)
|
||||
return JSON.parse(stateStr)
|
||||
}
|
||||
|
||||
|
||||
private async _storeState() {
|
||||
const state = this.state
|
||||
const encryptedState = await this._encryptionService.encrypt(JSON.stringify(state))
|
||||
this._storageService.store(STORAGE_KEY, encryptedState, StorageScope.APPLICATION, StorageTarget.USER);
|
||||
}
|
||||
|
||||
setSettingOfProvider: SetSettingOfProviderFn = async (providerName, settingName, newVal) => {
|
||||
|
||||
const newModelSelectionOfFeature = this.state.modelSelectionOfFeature
|
||||
|
||||
const newSettingsOfProvider = {
|
||||
...this.state.settingsOfProvider,
|
||||
[providerName]: {
|
||||
...this.state.settingsOfProvider[providerName],
|
||||
[settingName]: newVal,
|
||||
}
|
||||
}
|
||||
|
||||
const newGlobalSettings = this.state.globalSettings
|
||||
|
||||
// if changed models or enabled a provider, recompute models list
|
||||
const modelsListChanged = settingName === 'models' || settingName === '_enabled'
|
||||
const newModelsList = modelsListChanged ? _computeModelOptions(newSettingsOfProvider) : this.state._modelOptions
|
||||
|
||||
const newState: VoidSettingsState = {
|
||||
modelSelectionOfFeature: newModelSelectionOfFeature,
|
||||
settingsOfProvider: newSettingsOfProvider,
|
||||
globalSettings: newGlobalSettings,
|
||||
_modelOptions: newModelsList,
|
||||
}
|
||||
|
||||
// this must go above this.setanythingelse()
|
||||
this.state = newState
|
||||
|
||||
// if the user-selected model is no longer in the list, update the selection for each feature that needs it to something relevant (the 0th model available, or null)
|
||||
if (modelsListChanged) {
|
||||
for (const featureName of featureNames) {
|
||||
|
||||
const currentSelection = newModelSelectionOfFeature[featureName]
|
||||
const selnIdx = currentSelection === null ? -1 : newModelsList.findIndex(m => modelSelectionsEqual(m.selection, currentSelection))
|
||||
|
||||
if (selnIdx === -1) {
|
||||
if (newModelsList.length !== 0)
|
||||
this.setModelSelectionOfFeature(featureName, newModelsList[0].selection, { doNotApplyEffects: true })
|
||||
else
|
||||
this.setModelSelectionOfFeature(featureName, null, { doNotApplyEffects: true })
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('settingsOfProvider')
|
||||
}
|
||||
|
||||
|
||||
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
globalSettings: {
|
||||
...this.state.globalSettings,
|
||||
[settingName]: newVal
|
||||
}
|
||||
}
|
||||
this.state = newState
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire(['globalSettings', settingName])
|
||||
|
||||
}
|
||||
|
||||
|
||||
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn = async (featureName, newVal, options) => {
|
||||
const newState: VoidSettingsState = {
|
||||
...this.state,
|
||||
modelSelectionOfFeature: {
|
||||
...this.state.modelSelectionOfFeature,
|
||||
[featureName]: newVal
|
||||
}
|
||||
}
|
||||
|
||||
this.state = newState
|
||||
|
||||
if (options?.doNotApplyEffects)
|
||||
return
|
||||
|
||||
await this._storeState()
|
||||
this._onDidChangeState.fire('modelSelectionOfFeature')
|
||||
}
|
||||
|
||||
|
||||
|
||||
setAutodetectedModels(providerName: ProviderName, newDefaultModelNames: string[], logging: object) {
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
|
||||
const old_names = models.map(m => m.modelName)
|
||||
|
||||
const newDefaultModels = modelInfoOfDefaultNames(newDefaultModelNames, { isAutodetected: true, existingModels: models })
|
||||
const newModels = [
|
||||
...newDefaultModels,
|
||||
...models.filter(m => !m.isDefault), // keep any non-default models
|
||||
]
|
||||
|
||||
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
// if the models changed, log it
|
||||
const new_names = newModels.map(m => m.modelName)
|
||||
if (!(old_names.length === new_names.length
|
||||
&& old_names.every((_, i) => old_names[i] === new_names[i])
|
||||
)) {
|
||||
this._metricsService.capture('Autodetect Models', { providerName, newModels, ...logging })
|
||||
}
|
||||
}
|
||||
toggleModelHidden(providerName: ProviderName, modelName: string) {
|
||||
|
||||
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const modelIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (modelIdx === -1) return
|
||||
const newIsHidden = !models[modelIdx].isHidden
|
||||
const newModels: VoidModelInfo[] = [
|
||||
...models.slice(0, modelIdx),
|
||||
{ ...models[modelIdx], isHidden: newIsHidden },
|
||||
...models.slice(modelIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
this._metricsService.capture('Toggle Model Hidden', { providerName, modelName, newIsHidden })
|
||||
|
||||
}
|
||||
addModel(providerName: ProviderName, modelName: string) {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const existingIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (existingIdx !== -1) return // if exists, do nothing
|
||||
const newModels = [
|
||||
...models,
|
||||
{ modelName, isDefault: false, isHidden: false }
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
this._metricsService.capture('Add Model', { providerName, modelName })
|
||||
|
||||
}
|
||||
deleteModel(providerName: ProviderName, modelName: string): boolean {
|
||||
const { models } = this.state.settingsOfProvider[providerName]
|
||||
const delIdx = models.findIndex(m => m.modelName === modelName)
|
||||
if (delIdx === -1) return false
|
||||
const newModels = [
|
||||
...models.slice(0, delIdx), // delete the idx
|
||||
...models.slice(delIdx + 1, Infinity)
|
||||
]
|
||||
this.setSettingOfProvider(providerName, 'models', newModels)
|
||||
|
||||
this._metricsService.capture('Delete Model', { providerName, modelName })
|
||||
|
||||
return true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
|
||||
registerSingleton(IVoidSettingsService, VoidSettingsService, InstantiationType.Eager);
|
||||
@@ -0,0 +1,412 @@
|
||||
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
|
||||
|
||||
|
||||
export type VoidModelInfo = {
|
||||
modelName: string,
|
||||
isDefault: boolean, // whether or not it's a default for its provider
|
||||
isHidden: boolean, // whether or not the user is hiding it
|
||||
isAutodetected?: boolean, // whether the model was autodetected by polling
|
||||
}
|
||||
|
||||
// creates `modelInfo` from `modelNames`
|
||||
export const modelInfoOfDefaultNames = (modelNames: string[], options?: { isAutodetected: true, existingModels: VoidModelInfo[] }): VoidModelInfo[] => {
|
||||
|
||||
const { isAutodetected, existingModels } = options ?? {}
|
||||
|
||||
if (!existingModels) { // default settings
|
||||
|
||||
return modelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: modelNames.length >= 10 // hide all models if there are a ton of them, and make user enable them individually
|
||||
}))
|
||||
|
||||
} else { // settings if there are existing models (keep existing `isHidden` property)
|
||||
|
||||
const existingModelsMap: Record<string, VoidModelInfo> = {}
|
||||
for (const existingModel of existingModels) {
|
||||
existingModelsMap[existingModel.modelName] = existingModel
|
||||
}
|
||||
|
||||
return modelNames.map((modelName, i) => ({
|
||||
modelName,
|
||||
isDefault: true,
|
||||
isAutodetected: isAutodetected,
|
||||
isHidden: !!existingModelsMap[modelName]?.isHidden,
|
||||
}))
|
||||
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// https://docs.anthropic.com/en/docs/about-claude/models
|
||||
export const defaultAnthropicModels = modelInfoOfDefaultNames([
|
||||
'claude-3-5-sonnet-20241022',
|
||||
'claude-3-5-haiku-20241022',
|
||||
'claude-3-opus-20240229',
|
||||
'claude-3-sonnet-20240229',
|
||||
// 'claude-3-haiku-20240307',
|
||||
])
|
||||
|
||||
|
||||
// https://platform.openai.com/docs/models/gp
|
||||
export const defaultOpenAIModels = modelInfoOfDefaultNames([
|
||||
'o1-preview',
|
||||
'o1-mini',
|
||||
'gpt-4o',
|
||||
'gpt-4o-mini',
|
||||
// 'gpt-4o-2024-05-13',
|
||||
// 'gpt-4o-2024-08-06',
|
||||
// 'gpt-4o-mini-2024-07-18',
|
||||
// 'gpt-4-turbo',
|
||||
// 'gpt-4-turbo-2024-04-09',
|
||||
// 'gpt-4-turbo-preview',
|
||||
// 'gpt-4-0125-preview',
|
||||
// 'gpt-4-1106-preview',
|
||||
// 'gpt-4',
|
||||
// 'gpt-4-0613',
|
||||
// 'gpt-3.5-turbo-0125',
|
||||
// 'gpt-3.5-turbo',
|
||||
// 'gpt-3.5-turbo-1106',
|
||||
])
|
||||
|
||||
|
||||
|
||||
// https://console.groq.com/docs/models
|
||||
export const defaultGroqModels = modelInfoOfDefaultNames([
|
||||
"mixtral-8x7b-32768",
|
||||
"llama2-70b-4096",
|
||||
"gemma-7b-it"
|
||||
])
|
||||
|
||||
|
||||
export const defaultGeminiModels = modelInfoOfDefaultNames([
|
||||
'gemini-1.5-flash',
|
||||
'gemini-1.5-pro',
|
||||
'gemini-1.5-flash-8b',
|
||||
'gemini-1.0-pro'
|
||||
])
|
||||
|
||||
|
||||
|
||||
// export const parseMaxTokensStr = (maxTokensStr: string) => {
|
||||
// // parse the string but only if the full string is a valid number, eg parseInt('100abc') should return NaN
|
||||
// const int = isNaN(Number(maxTokensStr)) ? undefined : parseInt(maxTokensStr)
|
||||
// if (Number.isNaN(int))
|
||||
// return undefined
|
||||
// return int
|
||||
// }
|
||||
|
||||
|
||||
|
||||
|
||||
export const anthropicMaxPossibleTokens = (modelName: string) => {
|
||||
if (modelName === 'claude-3-5-sonnet-20241022'
|
||||
|| modelName === 'claude-3-5-haiku-20241022')
|
||||
return 8192
|
||||
if (modelName === 'claude-3-opus-20240229'
|
||||
|| modelName === 'claude-3-sonnet-20240229'
|
||||
|| modelName === 'claude-3-haiku-20240307')
|
||||
return 4096
|
||||
return 1024 // return a reasonably small number if they're using a different model
|
||||
}
|
||||
|
||||
|
||||
type UnionOfKeys<T> = T extends T ? keyof T : never;
|
||||
|
||||
|
||||
|
||||
export const defaultProviderSettings = {
|
||||
anthropic: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAI: {
|
||||
apiKey: '',
|
||||
},
|
||||
ollama: {
|
||||
endpoint: 'http://127.0.0.1:11434',
|
||||
},
|
||||
openRouter: {
|
||||
apiKey: '',
|
||||
},
|
||||
openAICompatible: {
|
||||
endpoint: '',
|
||||
apiKey: '',
|
||||
},
|
||||
gemini: {
|
||||
apiKey: '',
|
||||
},
|
||||
groq: {
|
||||
apiKey: ''
|
||||
}
|
||||
} as const
|
||||
|
||||
export type ProviderName = keyof typeof defaultProviderSettings
|
||||
export const providerNames = Object.keys(defaultProviderSettings) as ProviderName[]
|
||||
|
||||
export const localProviderNames = ['ollama'] satisfies ProviderName[] // all local names
|
||||
export const nonlocalProviderNames = providerNames.filter((name) => !(localProviderNames as string[]).includes(name)) // all non-local names
|
||||
|
||||
type CustomSettingName = UnionOfKeys<typeof defaultProviderSettings[ProviderName]>
|
||||
type CustomProviderSettings<providerName extends ProviderName> = {
|
||||
[k in CustomSettingName]: k extends keyof typeof defaultProviderSettings[providerName] ? string : undefined
|
||||
}
|
||||
export const customSettingNamesOfProvider = (providerName: ProviderName) => {
|
||||
return Object.keys(defaultProviderSettings[providerName]) as CustomSettingName[]
|
||||
}
|
||||
|
||||
|
||||
type CommonProviderSettings = {
|
||||
_enabled: boolean | undefined, // undefined initially, computed when user types in all fields
|
||||
models: VoidModelInfo[],
|
||||
}
|
||||
|
||||
export type SettingsForProvider<providerName extends ProviderName> = CustomProviderSettings<providerName> & CommonProviderSettings
|
||||
|
||||
// part of state
|
||||
export type SettingsOfProvider = {
|
||||
[providerName in ProviderName]: SettingsForProvider<providerName>
|
||||
}
|
||||
|
||||
|
||||
export type SettingName = keyof SettingsForProvider<ProviderName>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
type DisplayInfoForProviderName = {
|
||||
title: string,
|
||||
desc?: string,
|
||||
}
|
||||
|
||||
export const displayInfoOfProviderName = (providerName: ProviderName): DisplayInfoForProviderName => {
|
||||
if (providerName === 'anthropic') {
|
||||
return {
|
||||
title: 'Anthropic',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openAI') {
|
||||
return {
|
||||
title: 'OpenAI',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
return {
|
||||
title: 'OpenRouter',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'ollama') {
|
||||
return {
|
||||
title: 'Ollama',
|
||||
|
||||
}
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
return {
|
||||
title: 'OpenAI-Compatible',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'gemini') {
|
||||
return {
|
||||
title: 'Gemini',
|
||||
}
|
||||
}
|
||||
else if (providerName === 'groq') {
|
||||
return {
|
||||
title: 'Groq',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
|
||||
}
|
||||
|
||||
type DisplayInfo = {
|
||||
title: string,
|
||||
placeholder: string,
|
||||
subTextMd?: string,
|
||||
}
|
||||
export const displayInfoOfSettingName = (providerName: ProviderName, settingName: SettingName): DisplayInfo => {
|
||||
if (settingName === 'apiKey') {
|
||||
return {
|
||||
title: 'API Key',
|
||||
placeholder: providerName === 'anthropic' ? 'sk-ant-key...' : // sk-ant-api03-key
|
||||
providerName === 'openAI' ? 'sk-proj-key...' :
|
||||
providerName === 'openRouter' ? 'sk-or-key...' : // sk-or-v1-key
|
||||
providerName === 'gemini' ? 'key...' :
|
||||
providerName === 'groq' ? 'gsk_key...' :
|
||||
providerName === 'openAICompatible' ? 'sk-key...' :
|
||||
'(never)',
|
||||
|
||||
subTextMd: providerName === 'anthropic' ? 'Get your [API Key here](https://console.anthropic.com/settings/keys).' :
|
||||
providerName === 'openAI' ? 'Get your [API Key here](https://platform.openai.com/api-keys).' :
|
||||
providerName === 'openRouter' ? 'Get your [API Key here](https://openrouter.ai/settings/keys).' :
|
||||
providerName === 'gemini' ? 'Get your [API Key here](https://aistudio.google.com/apikey).' :
|
||||
providerName === 'groq' ? 'Get your [API Key here](https://console.groq.com/keys).' :
|
||||
providerName === 'openAICompatible' ? 'Add any OpenAI-Compatible endpoint.' :
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
else if (settingName === 'endpoint') {
|
||||
return {
|
||||
title: providerName === 'ollama' ? 'Endpoint' :
|
||||
providerName === 'openAICompatible' ? 'baseURL' // (do not include /chat/completions)
|
||||
: '(never)',
|
||||
|
||||
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
|
||||
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
|
||||
: '(never)',
|
||||
|
||||
subTextMd: providerName === 'ollama' ? 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).' :
|
||||
undefined,
|
||||
}
|
||||
}
|
||||
else if (settingName === '_enabled') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
else if (settingName === 'models') {
|
||||
return {
|
||||
title: '(never)',
|
||||
placeholder: '(never)',
|
||||
}
|
||||
}
|
||||
|
||||
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
|
||||
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
|
||||
apiKey: undefined,
|
||||
endpoint: undefined,
|
||||
}
|
||||
|
||||
export const voidInitModelOptions = {
|
||||
anthropic: {
|
||||
models: defaultAnthropicModels,
|
||||
},
|
||||
openAI: {
|
||||
models: defaultOpenAIModels,
|
||||
},
|
||||
ollama: {
|
||||
models: [],
|
||||
},
|
||||
openRouter: {
|
||||
models: [], // any string
|
||||
},
|
||||
openAICompatible: {
|
||||
models: [],
|
||||
},
|
||||
gemini: {
|
||||
models: defaultGeminiModels,
|
||||
},
|
||||
groq: {
|
||||
models: defaultGroqModels,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
// used when waiting and for a type reference
|
||||
export const defaultSettingsOfProvider: SettingsOfProvider = {
|
||||
anthropic: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.anthropic,
|
||||
...voidInitModelOptions.anthropic,
|
||||
},
|
||||
openAI: {
|
||||
_enabled: undefined,
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAI,
|
||||
...voidInitModelOptions.openAI,
|
||||
},
|
||||
gemini: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.gemini,
|
||||
...voidInitModelOptions.gemini,
|
||||
_enabled: undefined,
|
||||
},
|
||||
groq: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.groq,
|
||||
...voidInitModelOptions.groq,
|
||||
_enabled: undefined,
|
||||
},
|
||||
ollama: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.ollama,
|
||||
...voidInitModelOptions.ollama,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openRouter: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openRouter,
|
||||
...voidInitModelOptions.openRouter,
|
||||
_enabled: undefined,
|
||||
},
|
||||
openAICompatible: {
|
||||
...defaultCustomSettings,
|
||||
...defaultProviderSettings.openAICompatible,
|
||||
...voidInitModelOptions.openAICompatible,
|
||||
_enabled: undefined,
|
||||
},
|
||||
}
|
||||
|
||||
|
||||
export type ModelSelection = { providerName: ProviderName, modelName: string }
|
||||
|
||||
export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => {
|
||||
return m1.modelName === m2.modelName && m1.providerName === m2.providerName
|
||||
}
|
||||
|
||||
// this is a state
|
||||
export type ModelSelectionOfFeature = {
|
||||
'Ctrl+L': ModelSelection | null,
|
||||
'Ctrl+K': ModelSelection | null,
|
||||
'Autocomplete': ModelSelection | null,
|
||||
}
|
||||
export type FeatureName = keyof ModelSelectionOfFeature
|
||||
export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete'] as const
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
// the models of these can be refreshed (in theory all can, but not all should)
|
||||
export const refreshableProviderNames = localProviderNames
|
||||
export type RefreshableProviderName = typeof refreshableProviderNames[number]
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
export type GlobalSettings = {
|
||||
autoRefreshModels: boolean;
|
||||
aiInstructions: string;
|
||||
}
|
||||
export const defaultGlobalSettings: GlobalSettings = {
|
||||
autoRefreshModels: true,
|
||||
aiInstructions: '',
|
||||
}
|
||||
|
||||
export type GlobalSettingName = keyof GlobalSettings
|
||||
export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSettingName[]
|
||||
|
||||
|
||||
@@ -0,0 +1,46 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { createDecorator } from '../../instantiation/common/instantiation.js';
|
||||
import { ProxyChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { IMainProcessService } from '../../ipc/common/mainProcessService.js';
|
||||
import { InstantiationType, registerSingleton } from '../../instantiation/common/extensions.js';
|
||||
|
||||
|
||||
|
||||
export interface IVoidUpdateService {
|
||||
readonly _serviceBrand: undefined;
|
||||
check: () => Promise<{ hasUpdate: true, message: string } | { hasUpdate: false } | null>;
|
||||
}
|
||||
|
||||
|
||||
export const IVoidUpdateService = createDecorator<IVoidUpdateService>('VoidUpdateService');
|
||||
|
||||
|
||||
// implemented by calling channel
|
||||
export class VoidUpdateService implements IVoidUpdateService {
|
||||
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly voidUpdateService: IVoidUpdateService;
|
||||
|
||||
constructor(
|
||||
@IMainProcessService mainProcessService: IMainProcessService, // (only usable on client side)
|
||||
) {
|
||||
// creates an IPC proxy to use metricsMainService.ts
|
||||
this.voidUpdateService = ProxyChannel.toService<IVoidUpdateService>(mainProcessService.getChannel('void-channel-update'));
|
||||
}
|
||||
|
||||
|
||||
|
||||
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
|
||||
check: IVoidUpdateService['check'] = async () => {
|
||||
const res = await this.voidUpdateService.check()
|
||||
return res
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IVoidUpdateService, VoidUpdateService, InstantiationType.Eager);
|
||||
|
||||
|
||||
@@ -0,0 +1,71 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Anthropic from '@anthropic-ai/sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { anthropicMaxPossibleTokens } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
// Anthropic
|
||||
type LLMMessageAnthropic = {
|
||||
role: 'user' | 'assistant';
|
||||
content: string;
|
||||
}
|
||||
export const sendAnthropicMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.anthropic
|
||||
|
||||
const maxTokens = anthropicMaxPossibleTokens(modelName)
|
||||
if (maxTokens === undefined) {
|
||||
onError({ message: `Please set a value for Max Tokens.`, fullError: null })
|
||||
return
|
||||
}
|
||||
|
||||
const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
|
||||
// find system messages and concatenate them
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// remove system messages for Anthropic
|
||||
const anthropicMessages = messages.filter(msg => msg.role !== 'system') as LLMMessageAnthropic[]
|
||||
|
||||
|
||||
|
||||
const stream = anthropic.messages.stream({
|
||||
system: systemMessage,
|
||||
messages: anthropicMessages,
|
||||
model: modelName,
|
||||
max_tokens: maxTokens,
|
||||
});
|
||||
|
||||
|
||||
// when receive text
|
||||
stream.on('text', (newText, fullText) => {
|
||||
onText({ newText, fullText })
|
||||
})
|
||||
|
||||
// when we get the final message on this stream (or when error/fail)
|
||||
stream.on('finalMessage', (claude_response) => {
|
||||
// stringify the response's content
|
||||
const content = claude_response.content.map(c => c.type === 'text' ? c.text : c.type).join('\n');
|
||||
onFinalMessage({ fullText: content })
|
||||
})
|
||||
|
||||
stream.on('error', (error) => {
|
||||
// the most common error will be invalid API key (401), so we handle this with a nice message
|
||||
if (error instanceof Anthropic.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error })
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error }) // anthropic errors can be stringified nicely like this
|
||||
}
|
||||
})
|
||||
|
||||
// TODO need to test this to make sure it works, it might throw an error
|
||||
_setAborter(() => stream.controller.abort())
|
||||
|
||||
};
|
||||
@@ -0,0 +1,53 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Content, GoogleGenerativeAI, GoogleGenerativeAIFetchError } from '@google/generative-ai';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Gemini
|
||||
export const sendGeminiMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const thisConfig = settingsOfProvider.gemini
|
||||
|
||||
const genAI = new GoogleGenerativeAI(thisConfig.apiKey);
|
||||
const model = genAI.getGenerativeModel({ model: modelName });
|
||||
|
||||
// remove system messages that get sent to Gemini
|
||||
// str of all system messages
|
||||
const systemMessage = messages
|
||||
.filter(msg => msg.role === 'system')
|
||||
.map(msg => msg.content)
|
||||
.join('\n');
|
||||
|
||||
// Convert messages to Gemini format
|
||||
const geminiMessages: Content[] = messages
|
||||
.filter(msg => msg.role !== 'system')
|
||||
.map((msg, i) => ({
|
||||
parts: [{ text: msg.content }],
|
||||
role: msg.role === 'assistant' ? 'model' : 'user'
|
||||
}))
|
||||
|
||||
model.generateContentStream({ contents: geminiMessages, systemInstruction: systemMessage, })
|
||||
.then(async response => {
|
||||
_setAborter(() => response.stream.return(fullText))
|
||||
|
||||
for await (const chunk of response.stream) {
|
||||
const newText = chunk.text();
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch((error) => {
|
||||
if (error instanceof GoogleGenerativeAIFetchError && error.status === 400) {
|
||||
onError({ message: 'Invalid API key.', fullError: null });
|
||||
}
|
||||
else {
|
||||
onError({ message: error + '', fullError: error });
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,68 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// // Greptile
|
||||
// // https://docs.greptile.com/api-reference/query
|
||||
// // https://docs.greptile.com/quickstart#sample-response-streamed
|
||||
|
||||
// import { SendLLMMessageFnTypeInternal } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// export const sendGreptileMsg: SendLLMMessageFnTypeInternal = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, _setAborter }) => {
|
||||
|
||||
// let fullText = ''
|
||||
|
||||
// const thisConfig = settingsOfProvider.greptile
|
||||
|
||||
// fetch('https://api.greptile.com/v2/query', {
|
||||
// method: 'POST',
|
||||
// headers: {
|
||||
// 'Authorization': `Bearer ${thisConfig.apikey}`,
|
||||
// 'X-Github-Token': `${thisConfig.githubPAT}`,
|
||||
// 'Content-Type': `application/json`,
|
||||
// },
|
||||
// body: JSON.stringify({
|
||||
// messages,
|
||||
// stream: true,
|
||||
// repositories: [thisConfig.repoinfo],
|
||||
// }),
|
||||
// })
|
||||
// // this is {message}\n{message}\n{message}...\n
|
||||
// .then(async response => {
|
||||
// const text = await response.text()
|
||||
// console.log('got greptile', text)
|
||||
// return JSON.parse(`[${text.trim().split('\n').join(',')}]`)
|
||||
// })
|
||||
// // TODO make this actually stream, right now it just sends one message at the end
|
||||
// // TODO add _setAborter() when add streaming
|
||||
// .then(async responseArr => {
|
||||
|
||||
// for (const response of responseArr) {
|
||||
// const type: string = response['type']
|
||||
// const message = response['message']
|
||||
|
||||
// // when receive text
|
||||
// if (type === 'message') {
|
||||
// fullText += message
|
||||
// onText({ newText: message, fullText })
|
||||
// }
|
||||
// else if (type === 'sources') {
|
||||
// const { filepath, linestart: _, lineend: _2 } = message as { filepath: string; linestart: number | null; lineend: number | null }
|
||||
// fullText += filepath
|
||||
// onText({ newText: filepath, fullText })
|
||||
// }
|
||||
// // type: 'status' with an empty 'message' means last message
|
||||
// else if (type === 'status') {
|
||||
// if (!message) {
|
||||
// onFinalMessage({ fullText })
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
// })
|
||||
// .catch(error => {
|
||||
// onError({ error })
|
||||
// });
|
||||
|
||||
// }
|
||||
@@ -0,0 +1,46 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import Groq from 'groq-sdk';
|
||||
import { _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
|
||||
// Groq
|
||||
export const sendGroqMsg: _InternalSendLLMMessageFnType = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
let fullText = '';
|
||||
|
||||
const thisConfig = settingsOfProvider.groq
|
||||
|
||||
const groq = new Groq({
|
||||
apiKey: thisConfig.apiKey,
|
||||
dangerouslyAllowBrowser: true
|
||||
});
|
||||
|
||||
await groq.chat.completions
|
||||
.create({
|
||||
messages: messages,
|
||||
model: modelName,
|
||||
stream: true,
|
||||
temperature: 0.7,
|
||||
// max_tokens: parseMaxTokensStr(thisConfig.maxTokens),
|
||||
})
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
if (newText) {
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
}
|
||||
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
.catch(error => {
|
||||
onError({ message: error + '', fullError: error });
|
||||
})
|
||||
|
||||
|
||||
};
|
||||
@@ -0,0 +1,86 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Ollama } from 'ollama';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType, OllamaModelResponse } from '../../common/llmMessageTypes.js';
|
||||
import { defaultProviderSettings } from '../../common/voidSettingsTypes.js';
|
||||
|
||||
export const ollamaList: _InternalModelListFnType<OllamaModelResponse> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
|
||||
const onSuccess = ({ models }: { models: OllamaModelResponse[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} in Void if you want the default url).`)
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
ollama.list()
|
||||
.then((response) => {
|
||||
const { models } = response
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
// Ollama
|
||||
export const sendOllamaMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter }) => {
|
||||
|
||||
const thisConfig = settingsOfProvider.ollama
|
||||
// if endpoint is empty, normally ollama will send to 11434, but we want it to fail - the user should type it in
|
||||
if (!thisConfig.endpoint) throw new Error(`Ollama Endpoint was empty (please enter ${defaultProviderSettings.ollama.endpoint} if you want the default).`)
|
||||
|
||||
let fullText = ''
|
||||
|
||||
const ollama = new Ollama({ host: thisConfig.endpoint })
|
||||
|
||||
ollama.chat({
|
||||
model: modelName,
|
||||
messages: messages,
|
||||
stream: true,
|
||||
// options: { num_predict: parseMaxTokensStr(thisConfig.maxTokens) } // this is max_tokens
|
||||
})
|
||||
.then(async stream => {
|
||||
_setAborter(() => stream.abort())
|
||||
// iterate through the stream
|
||||
for await (const chunk of stream) {
|
||||
const newText = chunk.message.content;
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
|
||||
})
|
||||
// when error/fail
|
||||
.catch((error) => {
|
||||
// if (typeof error === 'object') {
|
||||
// const e = error.error as ErrorResponse['error']
|
||||
// if (e) {
|
||||
// const name = error.name ?? 'Error'
|
||||
// onError({ error: `${name}: ${e}` })
|
||||
// return;
|
||||
// }
|
||||
// }
|
||||
onError({ message: error + '', fullError: error })
|
||||
})
|
||||
|
||||
};
|
||||
|
||||
|
||||
|
||||
// ['codestral', 'qwen2.5-coder', 'qwen2.5-coder:0.5b', 'qwen2.5-coder:1.5b', 'qwen2.5-coder:3b', 'qwen2.5-coder:7b', 'qwen2.5-coder:14b', 'qwen2.5-coder:32b', 'codegemma', 'codegemma:2b', 'codegemma:7b', 'codellama', 'codellama:7b', 'codellama:13b', 'codellama:34b', 'codellama:70b', 'codellama:code', 'codellama:python', 'command-r', 'command-r:35b', 'command-r-plus', 'command-r-plus:104b', 'deepseek-coder-v2', 'deepseek-coder-v2:16b', 'deepseek-coder-v2:236b', 'falcon2', 'falcon2:11b', 'firefunction-v2', 'firefunction-v2:70b', 'gemma', 'gemma:2b', 'gemma:7b', 'gemma2', 'gemma2:2b', 'gemma2:9b', 'gemma2:27b', 'llama2', 'llama2:7b', 'llama2:13b', 'llama2:70b', 'llama3', 'llama3:8b', 'llama3:70b', 'llama3-chatqa', 'llama3-chatqa:8b', 'llama3-chatqa:70b', 'llama3-gradient', 'llama3-gradient:8b', 'llama3-gradient:70b', 'llama3.1', 'llama3.1:8b', 'llama3.1:70b', 'llama3.1:405b', 'llava', 'llava:7b', 'llava:13b', 'llava:34b', 'llava-llama3', 'llava-llama3:8b', 'llava-phi3', 'llava-phi3:3.8b', 'mistral', 'mistral:7b', 'mistral-large', 'mistral-large:123b', 'mistral-nemo', 'mistral-nemo:12b', 'mixtral', 'mixtral:8x7b', 'mixtral:8x22b', 'moondream', 'moondream:1.8b', 'openhermes', 'openhermes:v2.5', 'phi3', 'phi3:3.8b', 'phi3:14b', 'phi3.5', 'phi3.5:3.8b', 'qwen', 'qwen:7b', 'qwen:14b', 'qwen:32b', 'qwen:72b', 'qwen:110b', 'qwen2', 'qwen2:0.5b', 'qwen2:1.5b', 'qwen2:7b', 'qwen2:72b', 'smollm', 'smollm:135m', 'smollm:360m', 'smollm:1.7b',]
|
||||
@@ -0,0 +1,105 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import OpenAI from 'openai';
|
||||
import { _InternalModelListFnType, _InternalSendLLMMessageFnType } from '../../common/llmMessageTypes.js';
|
||||
import { Model } from 'openai/resources/models.js';
|
||||
// import { parseMaxTokensStr } from './util.js';
|
||||
|
||||
|
||||
|
||||
export const openaiCompatibleList: _InternalModelListFnType<Model> = async ({ onSuccess: onSuccess_, onError: onError_, settingsOfProvider }) => {
|
||||
const onSuccess = ({ models }: { models: Model[] }) => {
|
||||
onSuccess_({ models })
|
||||
}
|
||||
|
||||
const onError = ({ error }: { error: string }) => {
|
||||
onError_({ error })
|
||||
}
|
||||
|
||||
try {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
const openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
|
||||
openai.models.list()
|
||||
.then(async (response) => {
|
||||
const models: Model[] = []
|
||||
models.push(...response.data)
|
||||
while (response.hasNextPage()) {
|
||||
models.push(...(await response.getNextPage()).data)
|
||||
}
|
||||
onSuccess({ models })
|
||||
})
|
||||
.catch((error) => {
|
||||
onError({ error: error + '' })
|
||||
})
|
||||
}
|
||||
catch (error) {
|
||||
onError({ error: error + '' })
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
// OpenAI, OpenRouter, OpenAICompatible
|
||||
export const sendOpenAIMsg: _InternalSendLLMMessageFnType = ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName }) => {
|
||||
|
||||
let fullText = ''
|
||||
|
||||
let openai: OpenAI
|
||||
let options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming
|
||||
|
||||
|
||||
if (providerName === 'openAI') {
|
||||
const thisConfig = settingsOfProvider.openAI
|
||||
openai = new OpenAI({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true });
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openRouter') {
|
||||
const thisConfig = settingsOfProvider.openRouter
|
||||
openai = new OpenAI({
|
||||
baseURL: 'https://openrouter.ai/api/v1', apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true,
|
||||
defaultHeaders: {
|
||||
'HTTP-Referer': 'https://voideditor.com', // Optional, for including your app on openrouter.ai rankings.
|
||||
'X-Title': 'Void Editor', // Optional. Shows in rankings on openrouter.ai.
|
||||
},
|
||||
});
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else if (providerName === 'openAICompatible') {
|
||||
const thisConfig = settingsOfProvider.openAICompatible
|
||||
openai = new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true })
|
||||
options = { model: modelName, messages: messages, stream: true, /*max_completion_tokens: parseMaxTokensStr(thisConfig.maxTokens)*/ }
|
||||
}
|
||||
else {
|
||||
console.error(`sendOpenAIMsg: invalid providerName: ${providerName}`)
|
||||
throw new Error(`providerName was invalid: ${providerName}`)
|
||||
}
|
||||
|
||||
openai.models.list()
|
||||
openai.chat.completions
|
||||
.create(options)
|
||||
.then(async response => {
|
||||
_setAborter(() => response.controller.abort())
|
||||
// when receive text
|
||||
for await (const chunk of response) {
|
||||
const newText = chunk.choices[0]?.delta?.content || '';
|
||||
fullText += newText;
|
||||
onText({ newText, fullText });
|
||||
}
|
||||
onFinalMessage({ fullText });
|
||||
})
|
||||
// when error/fail - this catches errors of both .create() and .then(for await)
|
||||
.catch(error => {
|
||||
if (error instanceof OpenAI.APIError && error.status === 401) {
|
||||
onError({ message: 'Invalid API key.', fullError: error });
|
||||
}
|
||||
else {
|
||||
onError({ message: error, fullError: error });
|
||||
}
|
||||
})
|
||||
|
||||
};
|
||||
@@ -0,0 +1,114 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { LLMMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js';
|
||||
import { IMetricsService } from '../../common/metricsService.js';
|
||||
|
||||
import { sendAnthropicMsg } from './anthropic.js';
|
||||
import { sendOllamaMsg } from './ollama.js';
|
||||
import { sendOpenAIMsg } from './openai.js';
|
||||
import { sendGeminiMsg } from './gemini.js';
|
||||
import { sendGroqMsg } from './groq.js';
|
||||
|
||||
export const sendLLMMessage = ({
|
||||
messages,
|
||||
onText: onText_,
|
||||
onFinalMessage: onFinalMessage_,
|
||||
onError: onError_,
|
||||
abortRef: abortRef_,
|
||||
logging: { loggingName },
|
||||
settingsOfProvider,
|
||||
providerName,
|
||||
modelName,
|
||||
}: LLMMMessageParams,
|
||||
|
||||
metricsService: IMetricsService
|
||||
) => {
|
||||
|
||||
// trim message content (Anthropic and other providers give an error if there is trailing whitespace)
|
||||
messages = messages.map(m => ({ ...m, content: m.content.trim() }))
|
||||
|
||||
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
|
||||
const captureChatEvent = (eventId: string, extras?: object) => {
|
||||
metricsService.capture(eventId, {
|
||||
providerName,
|
||||
modelName,
|
||||
numMessages: messages?.length,
|
||||
messagesShape: messages?.map(msg => ({ role: msg.role, length: msg.content.length })),
|
||||
...extras,
|
||||
})
|
||||
}
|
||||
const submit_time = new Date()
|
||||
|
||||
let _fullTextSoFar = ''
|
||||
let _aborter: (() => void) | null = null
|
||||
let _setAborter = (fn: () => void) => { _aborter = fn }
|
||||
let _didAbort = false
|
||||
|
||||
const onText: OnText = ({ newText, fullText }) => {
|
||||
if (_didAbort) return
|
||||
onText_({ newText, fullText })
|
||||
_fullTextSoFar = fullText
|
||||
}
|
||||
|
||||
const onFinalMessage: OnFinalMessage = ({ fullText }) => {
|
||||
if (_didAbort) return
|
||||
captureChatEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
|
||||
onFinalMessage_({ fullText })
|
||||
}
|
||||
|
||||
const onError: OnError = ({ message: error, fullError }) => {
|
||||
if (_didAbort) return
|
||||
console.error('sendLLMMessage onError:', error)
|
||||
captureChatEvent(`${loggingName} - Error`, { error })
|
||||
onError_({ message: error, fullError })
|
||||
}
|
||||
|
||||
const onAbort = () => {
|
||||
captureChatEvent(`${loggingName} - Abort`, { messageLengthSoFar: _fullTextSoFar.length })
|
||||
try { _aborter?.() } // aborter sometimes automatically throws an error
|
||||
catch (e) { }
|
||||
_didAbort = true
|
||||
}
|
||||
abortRef_.current = onAbort
|
||||
|
||||
captureChatEvent(`${loggingName} - Sending Message`, { messageLength: messages[messages.length - 1]?.content.length })
|
||||
|
||||
try {
|
||||
switch (providerName) {
|
||||
case 'anthropic':
|
||||
sendAnthropicMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'openAI':
|
||||
case 'openRouter':
|
||||
case 'openAICompatible':
|
||||
sendOpenAIMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'gemini':
|
||||
sendGeminiMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'ollama':
|
||||
sendOllamaMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
case 'groq':
|
||||
sendGroqMsg({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelName, _setAborter, providerName });
|
||||
break;
|
||||
default:
|
||||
onError({ message: `Error: Void provider was "${providerName}", which is not recognized.`, fullError: null })
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
catch (error) {
|
||||
if (error instanceof Error) { onError({ message: error + '', fullError: error }) }
|
||||
else { onError({ message: `Unexpected Error in sendLLMMessage: ${error}`, fullError: error }); }
|
||||
// ; (_aborter as any)?.()
|
||||
// _didAbort = true
|
||||
}
|
||||
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,140 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
// registered in app.ts
|
||||
// code convention is to make a service responsible for this stuff, and not a channel, but having fewer files is simpler...
|
||||
|
||||
import { IServerChannel } from '../../../base/parts/ipc/common/ipc.js';
|
||||
import { Emitter, Event } from '../../../base/common/event.js';
|
||||
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainLLMMessageParams, AbortRef, LLMMMessageParams, MainLLMMessageAbortParams, MainModelListParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, OpenaiCompatibleModelResponse, } from '../common/llmMessageTypes.js';
|
||||
import { sendLLMMessage } from './llmMessage/sendLLMMessage.js'
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { ollamaList } from './llmMessage/ollama.js';
|
||||
import { openaiCompatibleList } from './llmMessage/openai.js';
|
||||
|
||||
// NODE IMPLEMENTATION - calls actual sendLLMMessage() and returns listeners to it
|
||||
|
||||
export class LLMMessageChannel implements IServerChannel {
|
||||
// sendLLMMessage
|
||||
private readonly _onText_llm = new Emitter<EventLLMMessageOnTextParams>();
|
||||
private readonly _onFinalMessage_llm = new Emitter<EventLLMMessageOnFinalMessageParams>();
|
||||
private readonly _onError_llm = new Emitter<EventLLMMessageOnErrorParams>();
|
||||
|
||||
// abort
|
||||
private readonly _abortRefOfRequestId_llm: Record<string, AbortRef> = {}
|
||||
|
||||
// ollamaList
|
||||
private readonly _onSuccess_ollama = new Emitter<EventModelListOnSuccessParams<OllamaModelResponse>>();
|
||||
private readonly _onError_ollama = new Emitter<EventModelListOnErrorParams<OllamaModelResponse>>();
|
||||
|
||||
// openaiCompatibleList
|
||||
private readonly _onSuccess_openAICompatible = new Emitter<EventModelListOnSuccessParams<OpenaiCompatibleModelResponse>>();
|
||||
private readonly _onError_openAICompatible = new Emitter<EventModelListOnErrorParams<OpenaiCompatibleModelResponse>>();
|
||||
|
||||
// stupidly, channels can't take in @IService
|
||||
constructor(
|
||||
private readonly metricsService: IMetricsService,
|
||||
) { }
|
||||
|
||||
// browser uses this to listen for changes
|
||||
listen(_: unknown, event: string): Event<any> {
|
||||
if (event === 'onText_llm') {
|
||||
return this._onText_llm.event;
|
||||
}
|
||||
else if (event === 'onFinalMessage_llm') {
|
||||
return this._onFinalMessage_llm.event;
|
||||
}
|
||||
else if (event === 'onError_llm') {
|
||||
return this._onError_llm.event;
|
||||
}
|
||||
else if (event === 'onSuccess_ollama') {
|
||||
return this._onSuccess_ollama.event;
|
||||
}
|
||||
else if (event === 'onError_ollama') {
|
||||
return this._onError_ollama.event;
|
||||
}
|
||||
else if (event === 'onSuccess_openAICompatible') {
|
||||
return this._onSuccess_openAICompatible.event;
|
||||
}
|
||||
else if (event === 'onError_openAICompatible') {
|
||||
return this._onError_openAICompatible.event;
|
||||
}
|
||||
else {
|
||||
throw new Error(`Event not found: ${event}`);
|
||||
}
|
||||
}
|
||||
|
||||
// browser uses this to call
|
||||
async call(_: unknown, command: string, params: any): Promise<any> {
|
||||
try {
|
||||
if (command === 'sendLLMMessage') {
|
||||
this._callSendLLMMessage(params)
|
||||
}
|
||||
else if (command === 'abort') {
|
||||
this._callAbort(params)
|
||||
}
|
||||
else if (command === 'ollamaList') {
|
||||
this._callOllamaList(params)
|
||||
}
|
||||
else if (command === 'openAICompatibleList') {
|
||||
this._callOpenAICompatibleList(params)
|
||||
}
|
||||
else {
|
||||
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
|
||||
}
|
||||
}
|
||||
catch (e) {
|
||||
console.log('llmMessageChannel: Call Error:', e)
|
||||
}
|
||||
}
|
||||
|
||||
// the only place sendLLMMessage is actually called
|
||||
private async _callSendLLMMessage(params: MainLLMMessageParams) {
|
||||
const { requestId } = params;
|
||||
|
||||
if (!(requestId in this._abortRefOfRequestId_llm))
|
||||
this._abortRefOfRequestId_llm[requestId] = { current: null }
|
||||
|
||||
const mainThreadParams: LLMMMessageParams = {
|
||||
...params,
|
||||
onText: ({ newText, fullText }) => { this._onText_llm.fire({ requestId, newText, fullText }); },
|
||||
onFinalMessage: ({ fullText }) => { this._onFinalMessage_llm.fire({ requestId, fullText }); },
|
||||
onError: ({ message: error, fullError }) => { console.log('sendLLM: firing err'); this._onError_llm.fire({ requestId, message: error, fullError }); },
|
||||
abortRef: this._abortRefOfRequestId_llm[requestId],
|
||||
}
|
||||
sendLLMMessage(mainThreadParams, this.metricsService);
|
||||
}
|
||||
|
||||
private _callAbort(params: MainLLMMessageAbortParams) {
|
||||
const { requestId } = params;
|
||||
if (!(requestId in this._abortRefOfRequestId_llm)) return
|
||||
this._abortRefOfRequestId_llm[requestId].current?.()
|
||||
delete this._abortRefOfRequestId_llm[requestId]
|
||||
}
|
||||
|
||||
private _callOllamaList(params: MainModelListParams<OllamaModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OllamaModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_ollama.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_ollama.fire({ requestId, error }); },
|
||||
}
|
||||
ollamaList(mainThreadParams)
|
||||
}
|
||||
|
||||
private _callOpenAICompatibleList(params: MainModelListParams<OpenaiCompatibleModelResponse>) {
|
||||
const { requestId } = params;
|
||||
|
||||
const mainThreadParams: ModelListParams<OpenaiCompatibleModelResponse> = {
|
||||
...params,
|
||||
onSuccess: ({ models }) => { this._onSuccess_openAICompatible.fire({ requestId, models }); },
|
||||
onError: ({ error }) => { this._onError_openAICompatible.fire({ requestId, error }); },
|
||||
}
|
||||
openaiCompatibleList(mainThreadParams)
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
@@ -0,0 +1,100 @@
|
||||
/*--------------------------------------------------------------------------------------
|
||||
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
|
||||
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
|
||||
*--------------------------------------------------------------------------------------*/
|
||||
|
||||
import { Disposable } from '../../../base/common/lifecycle.js';
|
||||
import { isLinux, isMacintosh, isWindows } from '../../../base/common/platform.js';
|
||||
import { generateUuid } from '../../../base/common/uuid.js';
|
||||
import { IEnvironmentMainService } from '../../environment/electron-main/environmentMainService.js';
|
||||
|
||||
import { IProductService } from '../../product/common/productService.js';
|
||||
import { IStorageMainService } from '../../storage/electron-main/storageMainService.js';
|
||||
|
||||
import { IMetricsService } from '../common/metricsService.js';
|
||||
import { PostHog } from 'posthog-node'
|
||||
|
||||
|
||||
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
|
||||
|
||||
const VOID_MACHINE_STORAGE_KEY = 'void.machineId'
|
||||
|
||||
export class MetricsMainService extends Disposable implements IMetricsService {
|
||||
_serviceBrand: undefined;
|
||||
|
||||
private readonly client: PostHog
|
||||
|
||||
private readonly _initProperties: object
|
||||
|
||||
|
||||
// TODO we should eventually identify people based on email
|
||||
private get machineId() {
|
||||
const currVal = this._storageService.applicationStorage.get(VOID_MACHINE_STORAGE_KEY)
|
||||
if (currVal !== undefined) return currVal
|
||||
const newVal = generateUuid()
|
||||
this._storageService.applicationStorage.set(VOID_MACHINE_STORAGE_KEY, newVal)
|
||||
return newVal
|
||||
}
|
||||
|
||||
|
||||
constructor(
|
||||
@IProductService private readonly _productService: IProductService,
|
||||
@IStorageMainService private readonly _storageService: IStorageMainService,
|
||||
@IEnvironmentMainService private readonly _envMainService: IEnvironmentMainService,
|
||||
) {
|
||||
super()
|
||||
this.client = new PostHog('phc_UanIdujHiLp55BkUTjB1AuBXcasVkdqRwgnwRlWESH2', {
|
||||
host: 'https://us.i.posthog.com',
|
||||
})
|
||||
|
||||
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
|
||||
|
||||
const { commit, version, quality } = this._productService
|
||||
|
||||
const isDevMode = !this._envMainService.isBuilt // found in abstractUpdateService.ts
|
||||
|
||||
|
||||
// custom properties we identify
|
||||
this._initProperties = {
|
||||
commit,
|
||||
version,
|
||||
os,
|
||||
quality,
|
||||
distinctId: this.machineId,
|
||||
isDevMode,
|
||||
...this._getOSInfo(),
|
||||
}
|
||||
|
||||
const identifyMessage = {
|
||||
distinctId: this.machineId,
|
||||
properties: this._initProperties,
|
||||
}
|
||||
this.client.identify(identifyMessage)
|
||||
|
||||
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
|
||||
|
||||
}
|
||||
|
||||
_getOSInfo() {
|
||||
try {
|
||||
const { platform, arch } = process // see platform.ts
|
||||
return { platform, arch }
|
||||
}
|
||||
catch (e) {
|
||||
return { osInfo: { platform: '??', arch: '??' } }
|
||||
}
|
||||
}
|
||||
|
||||
capture: IMetricsService['capture'] = (event, params) => {
|
||||
const capture = { distinctId: this.machineId, event, properties: params } as const
|
||||
// console.log('full capture:', capture)
|
||||
this.client.capture(capture)
|
||||
}
|
||||
|
||||
|
||||
async getDebuggingProperties() {
|
||||
return this._initProperties
|
||||
}
|
||||
}
|
||||
|
||||
|
||||