Compare commits

...

352 Commits

Author SHA1 Message Date
Andrew Pareles 7631b41b27 comment 2025-03-30 20:45:42 -07:00
Joaquin Coromina 0294cb3b98 Removed the fall back, restored the original code and commented it out 2025-03-30 22:51:40 -04:00
Joaquin Coromina 88dc32a43e Merge branch 'main' into patch-2 2025-03-30 22:46:27 -04:00
Andrew Pareles 2dcd95cf2a Merge pull request #363 from bjoaquinc/patch-1
Fixed default url in package.json since this is used by app
2025-03-30 17:24:37 -07:00
Joaquin Coromina 499abdf26c Fixed serverConfig.ts to pull voidVersion instead of version 2025-03-30 18:40:41 -04:00
Joaquin Coromina 52e92ac3f9 Update ${NAME_OF_REPO} to binaries repo for accessing the remote host tarballs 2025-03-30 10:50:40 -04:00
Andrew Pareles 885ebc8528 Merge pull request #360 from bjoaquinc/updated-remote-ssh-for-binaries
Fixed the open-remote-ssh extension to point at binaries repo
2025-03-30 03:50:07 -07:00
Andrew Pareles 4f4ef694ff Merge pull request #362 from voideditor/minor-changes-mar
1.0.4 test
2025-03-29 22:01:10 -07:00
Andrew Pareles 8cc68989dd 1.0.4 test 2025-03-29 21:50:06 -07:00
Andrew Pareles 57795b3210 Merge pull request #361 from voideditor/minor-changes-mar
Minor changes mar
2025-03-29 21:21:46 -07:00
Andrew Pareles 68be4dcf57 minor UI 2025-03-29 21:21:31 -07:00
Andrew Pareles eb4d2a6c7b version=34.3.2 2025-03-29 21:21:24 -07:00
Joaquin Coromina 36277eaed8 updated the downlaod url to point at void reh 2025-03-29 17:43:12 -04:00
Joaquin Coromina ba4cac3fb6 Updated the DEFAULT_DOWNLOAD_URL_TEMPLATE to point at the new void binaries repo and made the string dynamic 2025-03-29 17:34:29 -04:00
Andrew Pareles 3ce2911e3d 1.0.3 2025-03-29 03:39:48 -07:00
Andrew Pareles 89dc3c8805 1.0.2 2025-03-29 03:38:19 -07:00
Andrew Pareles 1bbd6f59fe Merge pull request #358 from voideditor/minor-changes-mar
version -> voidVersion
2025-03-29 03:34:36 -07:00
Andrew Pareles bf6cb764ec version -> voidVersion 2025-03-29 03:34:20 -07:00
Andrew Pareles a2a265423f 1.0.3 2025-03-29 03:07:05 -07:00
Andrew Pareles 5f39e314b6 1.0.2 2025-03-29 02:42:59 -07:00
Andrew Pareles b7e87b4af8 1.0.3 2025-03-29 02:04:31 -07:00
Andrew Pareles b34b2f3d7b 1.0.2 2025-03-29 01:57:06 -07:00
Andrew Pareles 5b6bf42576 Merge pull request #357 from voideditor/minor-changes-mar
bump to 1.0.3 to test auto-updates
2025-03-29 01:18:11 -07:00
Andrew Pareles a72ca7868e 1.0.3 2025-03-29 01:14:31 -07:00
Andrew Pareles a0f43c90c4 Merge pull request #356 from voideditor/minor-changes-mar
revert to 1.0.2 for testing
2025-03-29 00:30:06 -07:00
Andrew Pareles 41b85052f0 revert to 1.0.2 for testing 2025-03-29 00:29:27 -07:00
Andrew Pareles 4120031e0d Merge pull request #355 from voideditor/minor-changes-mar
test update to 1.0.3
2025-03-28 23:38:56 -07:00
Andrew Pareles a499fa19fb test update to 1.0.3 2025-03-28 23:38:31 -07:00
Andrew Pareles 6867dabf09 Merge pull request #354 from voideditor/github-workflow
enable auto updates
2025-03-28 17:37:00 -07:00
Andrew Pareles 93875818f5 enable auto updates 2025-03-28 16:21:11 -07:00
Andrew Pareles d09223c362 Merge pull request #353 from voideditor/minor-changes-mar
dummy commit
2025-03-28 02:04:53 -07:00
Andrew Pareles fb5ae96a44 dummy commit 2025-03-28 02:03:47 -07:00
Andrew Pareles dbe62e82e1 Merge pull request #351 from voideditor/minor-changes-mar
new code.ico
2025-03-27 17:14:24 -07:00
Andrew Pareles e5c72ac13e new code.ico 2025-03-27 17:13:31 -07:00
Andrew Pareles dc143e7cb5 Merge pull request #350 from voideditor/minor-changes-mar
xpm
2025-03-27 17:04:36 -07:00
Andrew Pareles 80443892f3 xpm 2025-03-27 17:03:31 -07:00
Andrew Pareles 70e89c8f20 Merge pull request #349 from voideditor/github-workflow
fix vscodium patches
2025-03-27 15:56:10 -07:00
Andrew Pareles 57e8ef5463 fix vscodium patches 2025-03-26 23:14:43 -07:00
Andrew Pareles d5581b1bb4 Merge pull request #345 from voideditor/github-workflow
tslib
2025-03-26 21:43:04 -07:00
Andrew Pareles 1482ebba64 tslib 2025-03-26 21:42:26 -07:00
Andrew Pareles 6d4253dff9 Merge pull request #344 from voideditor/github-workflow
More vscodium patches
2025-03-26 21:01:50 -07:00
Andrew Pareles cc4c863c82 version-0 and version-1 2025-03-26 21:01:21 -07:00
Andrew Pareles 4618e81d20 Merge pull request #343 from voideditor/github-workflow
Vscodium patches
2025-03-26 19:58:53 -07:00
Andrew Pareles 7fa971cbcd Merge pull request #342 from voideditor/minor-changes-mar
Minor changes mar
2025-03-26 19:56:22 -07:00
Andrew Pareles 2267757c41 apply some patches 2025-03-26 19:53:02 -07:00
Andrew Pareles e07727393b rm workflows, move to its own dir 2025-03-26 14:55:08 -07:00
Andrew Pareles 4e56321eb9 update 2025-03-25 20:11:35 -07:00
Andrew Pareles 9e177c73ca + 2025-03-25 20:08:02 -07:00
Andrew Pareles ccbe1911d2 change build 2025-03-25 20:04:19 -07:00
Andrew Pareles a6d5a5f325 universal macOS 2025-03-24 22:53:54 -07:00
Andrew Pareles e566db8396 Merge remote-tracking branch 'origin/model-selection' into github-workflow 2025-03-24 19:49:22 -07:00
Andrew Pareles b52cf80bd5 update 2025-03-24 19:48:11 -07:00
Andrew Pareles f1a31246b2 b2 2025-03-24 19:46:19 -07:00
Andrew Pareles e33b3ceae1 add refetch 2025-03-24 19:43:14 -07:00
Andrew Pareles c4e78cd3ba size 2025-03-24 18:36:20 -07:00
Andrew Pareles fa1e74acf2 deps 2025-03-24 18:35:41 -07:00
Andrew Pareles 112a770f2c leading 1 2025-03-24 18:31:49 -07:00
Andrew Pareles b1bcac5a6d deps 2025-03-24 18:29:40 -07:00
Andrew Pareles ae79229c00 more retry options 2025-03-24 08:43:52 -07:00
Andrew Pareles 28ac708904 error message 2025-03-24 08:36:01 -07:00
Andrew Pareles 161b446bb2 test3 2025-03-24 08:26:57 -07:00
Andrew Pareles 072484dd5d v4 2025-03-24 07:58:31 -07:00
Andrew Pareles a86d3dc82b large 2025-03-24 00:47:29 -07:00
Andrew Pareles b141b2edfe revert 3.7 with large macos 2025-03-24 00:47:07 -07:00
Mathew Pareles cf9482bd76 add back dropdown animation which was removed 2025-03-23 23:41:28 -07:00
Andrew Pareles 4be2c3b6fd +? 2025-03-23 23:29:53 -07:00
Mathew Pareles 69a75f5c47 fix paste 2025-03-23 23:24:08 -07:00
Andrew Pareles 0ab4f8513b fix? 2025-03-23 23:23:48 -07:00
Andrew Pareles 93f8597dd4 rm ci 2025-03-23 23:21:10 -07:00
Andrew Pareles 0f0e56a263 + 2025-03-23 23:16:37 -07:00
Andrew Pareles 91a23bb2d3 asd 2025-03-23 23:15:17 -07:00
Andrew Pareles 19503d92b4 fix? 2025-03-23 23:13:47 -07:00
Andrew Pareles e2648bba3e fix? 2025-03-23 23:12:45 -07:00
Andrew Pareles 090eaba3aa test 3 2025-03-23 23:07:14 -07:00
Mathew Pareles 1f98e75a4f terminal better 2025-03-23 22:46:25 -07:00
Andrew Pareles a38b4c8ee0 revert to 3.7 again 2025-03-23 22:44:06 -07:00
Andrew Pareles 1def205b28 test2 2025-03-23 22:28:59 -07:00
Mathew Pareles 32bee63b50 fix prompting that made void use pathname_search over regular search 2025-03-23 22:26:09 -07:00
Mathew Pareles 4798bbdc86 prompt 2025-03-23 22:03:26 -07:00
Andrew Pareles a854a341d3 revert to 3.7 2025-03-23 20:56:47 -07:00
Andrew Pareles 1a4623589c rm. ci 2025-03-23 20:54:30 -07:00
Andrew Pareles 51b19a6831 update 2025-03-23 20:53:37 -07:00
Andrew Pareles b58ad5f876 update 2025-03-23 20:48:35 -07:00
Andrew Pareles 4951116a6c test 2025-03-23 20:46:02 -07:00
Andrew Pareles 46ca49e02b + 2025-03-23 20:43:57 -07:00
Andrew Pareles 9133d6ecf5 test 2025-03-23 20:42:33 -07:00
Andrew Pareles ad736edd9e 3.7 edits 2025-03-23 20:29:59 -07:00
Mathew Pareles 1c7494861d rename for better accuracy 2025-03-23 20:26:47 -07:00
Andrew Pareles 31163e8516 npm i 2025-03-23 20:20:41 -07:00
Andrew Pareles 853a26ae8e disable ci 2025-03-23 20:08:34 -07:00
Andrew Pareles 16c7aaf599 v4? 2025-03-23 20:04:35 -07:00
Mathew Pareles d6df2514e1 fix ugly colors 2025-03-23 20:01:05 -07:00
Andrew Pareles 2b33294a85 + 2025-03-23 19:44:54 -07:00
Andrew Pareles 3ecdae6ab6 rename 2025-03-23 19:43:39 -07:00
Mathew Pareles fd1c840618 fix ugly blue color 2025-03-23 19:35:26 -07:00
Mathew Pareles 71f8315e06 fix paste issue 2025-03-23 19:30:32 -07:00
Andrew Pareles 633f7ffee5 test new workflow 2025-03-23 19:30:22 -07:00
Mathew Pareles 35f53e66dd definition 2025-03-23 18:47:34 -07:00
Andrew Pareles 82851d519d slice 2025-03-23 00:31:38 -07:00
Andrew Pareles cfc18827bb Merge remote-tracking branch 'origin/main' into model-selection 2025-03-22 19:12:22 -07:00
Andrew Pareles 04e8a204a3 add trusted domain 2025-03-22 18:14:55 -07:00
Andrew Pareles 6fd514230c Merge pull request #326 from voideditor/model-selection
Small (or empty) file editing
2025-03-22 00:40:17 -07:00
Mathew Pareles f75b4f34d8 fixes 2025-03-22 00:39:22 -07:00
Mathew Pareles 6ecf7be826 fix search+replace on empty files 2025-03-22 00:02:06 -07:00
Andrew Pareles 47998e4ee5 Merge pull request #325 from voideditor/v1.0.2
V1.0.2
2025-03-21 22:41:28 -07:00
Andrew Pareles 995f39b61f No X button 2025-03-21 22:38:26 -07:00
Andrew Pareles ec2ae0e190 more persistent update message 2025-03-21 22:31:24 -07:00
Andrew Pareles 2bef719754 Merge pull request #324 from voideditor/model-selection
Command Bar for Changes + Tool UI
2025-03-21 21:57:02 -07:00
Andrew Pareles c6376eb1db 1 2025-03-21 21:55:20 -07:00
Andrew Pareles db2c60db82 rm 2025-03-21 21:53:42 -07:00
Andrew Pareles a6f1b1e0ad window 2025-03-21 21:52:53 -07:00
Andrew Pareles 9edca589a0 1.0.2 2025-03-21 21:49:08 -07:00
Andrew Pareles da6710a079 current file fix will vs did 2025-03-21 21:45:17 -07:00
Andrew Pareles f60d5cad2f void agent 2025-03-21 21:22:21 -07:00
Andrew Pareles 75f1a2d79b format 2025-03-21 21:20:57 -07:00
Andrew Pareles 22dd42ba70 misc improvements 2025-03-21 21:20:41 -07:00
Mathew Pareles 1675879fa4 ux for commandbar 2025-03-21 20:14:55 -07:00
Andrew Pareles d6a7921526 Merge remote-tracking branch 'origin/main' into model-selection 2025-03-21 19:58:41 -07:00
Andrew Pareles 00734ce327 update commandbar 2025-03-21 19:58:41 -07:00
Mathew Pareles 3e584d01a3 focus on chatmessage edit 2025-03-21 19:53:12 -07:00
Mathew Pareles 847b57c406 bug 2025-03-21 19:40:59 -07:00
Andrew Pareles d4e9dbb82a rm titles and add .fsPaths 2025-03-21 19:33:03 -07:00
Andrew Pareles f3451d5077 command bar state + accept/reject all 2025-03-21 19:23:06 -07:00
Mathew Pareles 159d52a716 styles 2025-03-21 19:21:13 -07:00
Mathew Pareles 29f8fa6e1d improve selections 2025-03-21 19:09:49 -07:00
Andrew Pareles 9dc7632d9b Merge pull request #279 from jcommaret/voidVersion-from-tag
⚙️ - Void version from : tag (semver) commit and date
2025-03-21 15:25:49 -07:00
Andrew Pareles e1d16b3de4 + 2025-03-21 15:23:40 -07:00
Andrew Pareles 4301eb2bb1 version 2025-03-21 15:21:23 -07:00
Andrew Pareles 4ad82cff07 format 2025-03-21 15:19:07 -07:00
Andrew Pareles 22a16974f9 keep versioning, revert mistral (will review separately) 2025-03-21 15:18:29 -07:00
Andrew Pareles cf9eca0564 copy 2025-03-21 14:47:17 -07:00
Mathew Pareles 2eafda9132 fix opacity 2025-03-21 06:44:17 -07:00
Andrew Pareles c889acadc0 interruption handling 2025-03-21 06:44:05 -07:00
Mathew Pareles b3522ad73c better styling for commandbar 2025-03-21 06:42:07 -07:00
Andrew Pareles 96867326ae prompt 2025-03-21 05:59:49 -07:00
Andrew Pareles 4946d3c2a0 copy 2025-03-21 05:56:18 -07:00
Andrew Pareles 6a6cb56d87 add visual feedback for tool that's being loaded (eg edit tool) 2025-03-21 05:50:31 -07:00
Andrew Pareles eed71d9582 minor changes 2025-03-21 04:18:25 -07:00
Andrew Pareles 90a282a1f7 minor prompt fix 2025-03-21 02:54:52 -07:00
Andrew Pareles 8511681710 fixed events and interrupts during agent mode 2025-03-21 02:54:52 -07:00
Mathew Pareles 4dff3e216a command bar decent 2025-03-21 00:22:53 -07:00
Mathew Pareles e894be803b command bar draft 2025-03-21 00:22:53 -07:00
Andrew Pareles 07e995fc2f strict:true 2025-03-20 07:24:57 -07:00
Andrew Pareles cffe053226 merge 2025-03-20 06:03:37 -07:00
Andrew Pareles dafc509fa1 always add tool_request 2025-03-20 05:56:56 -07:00
Mathew Pareles 794c4593e6 styles 2025-03-20 04:44:10 -07:00
Mathew Pareles 5d825ad970 styles better 2025-03-20 04:43:06 -07:00
Andrew Pareles cc4115c8c4 misc fixes 2025-03-20 04:19:33 -07:00
Andrew Pareles b86356d2af misc bug fixes with oai-compat and agent 2025-03-20 04:03:37 -07:00
Andrew Pareles 8dab3ac854 tool error styles 2025-03-20 02:52:44 -07:00
Andrew Pareles 1326f19a5e add openrouter claude thinking 2025-03-20 02:37:04 -07:00
Andrew Pareles 69697542ba misc fixes 2025-03-20 00:46:10 -07:00
Andrew Pareles a7728a6030 show current tool running + misc style fixes + chat interrupt stops tool too 2025-03-19 23:00:26 -07:00
Andrew Pareles 9ee14e6a71 tense 2025-03-19 23:00:26 -07:00
Mathew Pareles 2ae098ee96 improvements 2025-03-19 22:19:51 -07:00
Mathew Pareles 0dae638be7 add automatic file adding 2025-03-19 21:58:14 -07:00
Andrew Pareles dd321121e3 prompt 2025-03-19 18:55:13 -07:00
Andrew Pareles 932b4974e9 prompt 2025-03-19 18:30:42 -07:00
Andrew Pareles 20d89cd07c prompt update 2025-03-19 18:28:38 -07:00
Andrew Pareles f75dee39b3 fix 2025-03-19 17:44:53 -07:00
Andrew Pareles acd711f93b fix 2025-03-19 16:32:36 -07:00
Andrew Pareles 744d387fe1 editCodeService and voidCommandBar service!!! + VoidCommandBar.tsx 2025-03-19 16:15:09 -07:00
Mathew Pareles ec505b653b debug fastapply mutiple changes (unsolved) 2025-03-18 22:35:57 -07:00
Mathew Pareles ad471a3015 fixes 2025-03-18 21:22:13 -07:00
Andrew Pareles 86fddaa0e0 text_search 2025-03-18 04:56:59 -07:00
Andrew Pareles b9405a76cd auto-approve 2025-03-18 04:51:23 -07:00
Mathew Pareles 730758cbec small changes.. 2025-03-18 03:43:12 -07:00
Andrew Pareles b26b62998d settings page 2025-03-18 03:42:53 -07:00
Andrew Pareles 6856e23e2a just revert all SEARCH/REPLACE on error + history reorder 2025-03-18 03:35:54 -07:00
Andrew Pareles b305fc5034 keep-conflicts on apply 2025-03-18 02:41:45 -07:00
Andrew Pareles 9ab3ec3c11 add autoapply 2025-03-18 00:33:40 -07:00
Andrew Pareles 5b3471a7d7 changes + potentially fix error finding SEARCH block 2025-03-17 23:47:35 -07:00
Mathew Pareles 6bb0966eea draft 2025-03-17 22:58:32 -07:00
Andrew Pareles 5d0a4c8157 misc 2025-03-17 22:51:23 -07:00
Andrew Pareles a1c7b254d3 flex instead of observer 2025-03-17 22:38:45 -07:00
Andrew Pareles 4cf79f7a3b styles 2025-03-17 22:25:41 -07:00
Andrew Pareles b1e50798d8 apply improvements 2025-03-17 20:55:37 -07:00
Andrew Pareles a320323aa1 promise resolves when abort to free space 2025-03-17 18:39:14 -07:00
Andrew Pareles ad2e5f2616 misc 2025-03-17 16:26:33 -07:00
Andrew Pareles f64f6b54b4 styles + fixes 2025-03-17 16:18:26 -07:00
Jérôme Commaret 6d8f843b12 Merge branch 'main' into voidVersion-from-tag 2025-03-17 18:25:35 +01:00
Jérôme Commaret a96e2f9a08 Update scripts/update-version.js
let try this.

Co-authored-by: ellipsis-dev[bot] <65095814+ellipsis-dev[bot]@users.noreply.github.com>
2025-03-17 18:20:56 +01:00
Jérôme Commaret e08e728fc8 Bump: voidVersion 2025-03-17 17:34:37 +01:00
Jérôme Commaret 18d2d9f5ee Merge branch 'dev' into voidVersion-from-tag 2025-03-17 17:33:30 +01:00
Andrew Pareles f2caa9876c isRunning: tool | message 2025-03-17 04:33:00 -07:00
Andrew Pareles 4a06d79170 type fix 2025-03-17 04:09:31 -07:00
Andrew Pareles 9a8814cc50 jump to file for edit request 2025-03-17 04:00:18 -07:00
Andrew Pareles d6327f2b03 explicit dependence on threadId in agentloop 2025-03-17 03:47:44 -07:00
Andrew Pareles 4528270937 add Void in file menu 2025-03-17 03:00:56 -07:00
Andrew Pareles 8ef693b218 comment 2025-03-17 02:47:11 -07:00
Andrew Pareles 46db2c6e95 + 2025-03-17 02:46:38 -07:00
Andrew Pareles 6b897a6f71 state simplify 2025-03-17 02:44:49 -07:00
Andrew Pareles 7aecbe25cd minor change 2025-03-17 02:39:55 -07:00
Andrew Pareles f79d438084 update styles 2025-03-17 02:19:51 -07:00
Andrew Pareles 35eb93d829 finally bg edits seem to actually work 2025-03-17 01:06:37 -07:00
Andrew Pareles 9350c0dcdf misc state fixes and fix model background (add voidModelService, delete voidFileService) 2025-03-16 23:19:22 -07:00
Mathew Pareles 39e989d3cc codespan link 2025-03-16 22:48:17 -07:00
Mathew Pareles a3cfd0d023 fix codespan links 2025-03-16 22:29:17 -07:00
Jérôme Commaret 7b3a82f1d2 Removed log 2025-03-16 21:13:57 +01:00
Jérôme Commaret 17ac9c081c Mistral FIM Method : seems to be the good one 2025-03-16 21:07:04 +01:00
Andrew Pareles 97b44a94e4 redesign how tool use loop works 2025-03-16 02:55:47 -07:00
Mathew Pareles 4a9af17736 commandbar draft (broken) 2025-03-15 21:55:14 -07:00
Mathew Pareles a33160f33e loading 2025-03-15 21:55:14 -07:00
Mathew Pareles 4bd6f4f8ac loading symbol Cmd+K 2025-03-15 21:54:48 -07:00
Mathew Pareles 71dc1ab93e change default setting for tailwind autosuggestions 2025-03-15 21:54:48 -07:00
Mathew Pareles 0ec9e4a54e codespan shorten paths 2025-03-15 21:54:48 -07:00
Mathew Pareles 4284461b06 fix height issue 2025-03-15 21:54:22 -07:00
Mathew Pareles 3f7f2fb40c commandbar draft 2025-03-15 21:54:22 -07:00
Andrew Pareles 47e93d1cf3 fix up settings 2025-03-15 17:00:21 -07:00
Andrew Pareles a99d1c99a5 agent mode must use tool models only 2025-03-15 16:23:14 -07:00
Andrew Pareles 7b76df235a reasoning component when streaming 2025-03-15 16:00:03 -07:00
Jérôme Commaret c6d246f783 main branch conflicts fixed 2025-03-15 16:07:33 +01:00
Jérôme Commaret ec8eb8bded Merge branch 'main' into dev 2025-03-15 15:02:41 +01:00
Jérôme Commaret 39fb3bc304 Merge branch 'feat_mistral-199' into dev 2025-03-15 14:01:18 +01:00
Andrew Pareles aca5deb814 desc 2025-03-15 04:24:39 -07:00
Andrew Pareles 824de11f21 setting 2025-03-15 04:21:01 -07:00
Andrew Pareles a4d93ac95a chatMode and agent mode UI 2025-03-15 04:19:30 -07:00
Andrew Pareles c1e83e458b header uri name 2025-03-15 03:42:21 -07:00
Andrew Pareles 7ea5870a11 alive ping 2025-03-15 03:38:36 -07:00
Andrew Pareles cf6cef8e0e massively improve styles 2025-03-15 03:24:48 -07:00
Andrew Pareles bf2843b1bd edit state and apply state should work, edit UI 2025-03-15 02:59:32 -07:00
Andrew Pareles 8a6b75b6bb style updates progress 2025-03-15 01:17:13 -07:00
Andrew Pareles e7e5e5ebf8 Merge pull request #320 from ZorinFoss/main
Enhance appimage creation
2025-03-14 19:46:01 -07:00
Jérôme Commaret 00e57dd1bf Merge pull request #3 from jcommaret/feat_mistral-199
Feat mistral 199
2025-03-14 23:23:15 +01:00
Jérôme Commaret 744841b092 Merge branch 'main' into feat_mistral-199 2025-03-14 23:22:37 +01:00
Jérôme Commaret 136840cca4 😻 : Miaou ! 2025-03-14 11:54:43 +01:00
Andrew Pareles dbdd40bd3d add better settings 2025-03-14 02:23:58 -07:00
Andrew Pareles 8f3a752562 sync fast apply to chat 2025-03-14 00:41:17 -07:00
Andrew Pareles fa43a9f937 language detection 2025-03-14 00:20:03 -07:00
Andrew Pareles c222a801a8 fix CtrlK model selection 2025-03-13 21:29:45 -07:00
Andrew Pareles def78affb4 ensureModelExists (forceModel) and fix quick edit state 2025-03-13 21:05:44 -07:00
Andrew Pareles edefe841d5 changes in the BG work!! + autosave 2025-03-13 16:54:31 -07:00
Jérôme Commaret 27b782ff32 HOURRA ! 2025-03-13 18:24:05 +01:00
Jérôme Commaret 654b577ce6 Looks really hacky, but working ! YES ! 2025-03-13 18:23:05 +01:00
Jérôme Commaret 8dcc3bf187 fix to do to types of Mistral connexion 2025-03-13 04:04:51 +01:00
Mathew Pareles cd512402d9 collab 2025-03-12 14:34:58 -07:00
Jérôme Commaret bc6f68e105 Hope it works 2025-03-12 18:52:54 +01:00
Jérôme Commaret b7f764799c stuff changed there also 2025-03-12 15:34:35 +01:00
Jérôme Commaret 6e39b225b2 - FIM is seems ok but disabled (have to find where to change it)
- Send Mistral chat to fix [error 422]
2025-03-12 15:34:02 +01:00
Andrew Pareles 744f0ed852 style 2025-03-12 05:33:38 -07:00
Andrew Pareles 1b6d81f4e0 approve tool works even if closed void and reopened 2025-03-12 05:16:42 -07:00
Mathew Pareles bb370b390c logic 2025-03-12 05:06:16 -07:00
Mathew Pareles 378ef493b5 fix gratuitous error 2025-03-12 04:35:12 -07:00
Mathew Pareles a70f606362 fix faulty changes 2025-03-12 03:41:08 -07:00
Andrew Pareles f66fbcd911 agentLoop improvements 2025-03-12 03:19:24 -07:00
Mathew Pareles 1c2e8c0c59 minh 2025-03-12 03:09:00 -07:00
Mathew Pareles 09ba9387bc fix refactor 2025-03-12 02:54:36 -07:00
Andrew Pareles b97c5089ce update approve/reject state 2025-03-12 02:26:55 -07:00
Andrew Pareles 7563f595f3 misc 2025-03-12 02:06:30 -07:00
Andrew Pareles 79ef6b5773 fix stream state 2025-03-12 02:02:25 -07:00
Andrew Pareles 11d376325e tool UI progress + terminal fix + misc fixes 2025-03-12 01:24:21 -07:00
Andrew Pareles 1ef8011bb0 tool UI step 1 (BROKEN) 2025-03-12 00:51:17 -07:00
Mathew Pareles fa1f407b76 fix naming for tool response 2025-03-12 00:37:30 -07:00
Mathew Pareles 094e05070a size 2025-03-12 00:20:21 -07:00
Mathew Pareles 6d5d4c34d7 size 2025-03-12 00:11:14 -07:00
Mathew Pareles 967f7dc85e make apply actually work 2025-03-12 00:06:28 -07:00
Jérôme Commaret d541d8fbb6 Merge branch 'voideditor:main' into main 2025-03-12 01:45:00 +01:00
Jérôme Commaret 89ac7738e4 sendLLMMessageToProviverImplementation : Mistral SendChat 2025-03-11 16:21:49 +01:00
Jérôme Commaret 7aca03de5c ADD : defaultModelsOfProvider : mistral - codestral-latest 2025-03-11 16:19:34 +01:00
Jérôme Commaret 4e9dc5ce4a defaultProviderSettings : mistral option 2025-03-11 16:12:14 +01:00
Andrew Pareles d6d5f77183 terminal progress + misc 2025-03-11 06:53:54 -07:00
Zorin FOSS 62b3a7d7be Add files via upload 2025-03-11 06:25:39 +02:00
Zorin FOSS 437244540a Enhance appimage script 2025-03-11 06:25:04 +02:00
Zorin FOSS 05d102f354 Enhance void.desktop 2025-03-11 05:27:42 +02:00
Andrew Pareles e4b0061f66 Merge remote-tracking branch 'origin/model-selection' 2025-03-10 19:14:29 -07:00
Jérôme Commaret e70e5d4208 Merge branch 'voideditor:main' into main 2025-03-11 00:36:08 +01:00
Andrew Pareles ccd0db635b Merge pull request #319 from voideditor/model-selection
Terminal Service Progress + Fix Import Issue
2025-03-10 15:38:39 -07:00
Andrew Pareles ab81479340 terminalService 2025-03-10 15:36:00 -07:00
Andrew Pareles 65be47d970 ancilary terminal tool 2025-03-10 15:36:00 -07:00
Mathew Pareles 217a33cc2e tool call improvements (error message + better ux) 2025-03-10 00:39:50 -07:00
Andrew Pareles 6a55abcd81 split into common/ and browser/ so terminal works 2025-03-09 18:09:35 -07:00
Jérôme Commaret 330092d023 mistral npm updated 2025-03-09 21:55:20 +01:00
Jérôme Commaret 7b665d05cf Merge pull request #1 from jcommaret/fix-pb
Fix pb
2025-03-09 05:35:38 +01:00
Jérôme Commaret 352496fc3a Merge branch 'main' into fix-pb 2025-03-09 05:35:16 +01:00
Jérôme Commaret eda11f29d9 FIM : I think it works as expected 2025-03-09 03:30:23 +01:00
Mathew Pareles 2300559081 links to code 2025-03-08 03:09:50 -08:00
Mathew Pareles 4a9c0c864f links draft 1 2025-03-08 03:09:50 -08:00
Andrew Pareles 4212e50ac5 Merge pull request #312 from voideditor/model-selection
Tool use improvements + Clear settings
2025-03-08 02:51:25 -08:00
Andrew Pareles 009dfbef37 improve prompt 2025-03-08 02:24:39 -08:00
Andrew Pareles 75ef9d5528 rm id and add tool_use_id 2025-03-07 23:59:58 -08:00
Jérôme Commaret ec4c52aff9 remove package 2025-03-08 08:23:40 +01:00
Jérôme Commaret bccc6a64ef Mistral FIM maybe 2025-03-08 08:23:20 +01:00
Andrew Pareles 93c0bb1d04 rename 2025-03-07 23:03:11 -08:00
Andrew Pareles 4061e6382a rm console.logs 2025-03-07 22:58:17 -08:00
Andrew Pareles b5dbd42eed make it so model does not change during agent mode even if user switches 2025-03-07 22:55:06 -08:00
Andrew Pareles c42a782bde rm void- 2025-03-07 20:56:03 -08:00
Andrew Pareles 6be0b74047 update 2025-03-07 20:55:42 -08:00
Andrew Pareles 0fb9222f3a switch 2025-03-07 20:54:55 -08:00
Andrew Pareles db7ec0272a don't display empty assistant messages (these are only used for LLMMessage) 2025-03-07 20:48:57 -08:00
Andrew Pareles 7558d4dc1c anthropic reasoning works with tools 2025-03-07 20:28:23 -08:00
Andrew Pareles f89b036d17 fix empty message order 2025-03-07 19:37:39 -08:00
Andrew Pareles 2b7df8c604 remove rawAnthropicContent 2025-03-07 19:37:33 -08:00
Jérôme Commaret ac2fb9e36d Mistral : Added, not fim 2025-03-07 14:30:33 +01:00
Andrew Pareles 5ad5fe7014 clickable tool requests 2025-03-07 03:14:20 -08:00
Andrew Pareles 5eb2bcfef6 Merge pull request #310 from voideditor/model-selection
Tool use progress + UI
2025-03-07 03:08:22 -08:00
Andrew Pareles cd532435c2 tool use is getting close! minor fixes + UI 2025-03-07 03:07:06 -08:00
Andrew Pareles 6e2a803628 tool use UI 2025-03-07 02:20:03 -08:00
Andrew Pareles 1e5e3d7f01 update prompt 2025-03-07 01:18:03 -08:00
Andrew Pareles ae15fd1f38 thinking slider backend support, QwQ 2025-03-07 00:28:54 -08:00
Andrew Pareles bc1a9d692e qwq + groq reasoning 2025-03-06 19:24:14 -08:00
Andrew Pareles dce3f392ce force placeholder color 2025-03-06 18:52:30 -08:00
Andrew Pareles 6d3de34818 style 2025-03-06 04:38:17 -08:00
Andrew Pareles f292fa8df6 xxs 2025-03-06 03:43:11 -08:00
Andrew Pareles 82ffd4e8f4 slider styles 2025-03-06 03:34:57 -08:00
Andrew Pareles 6b5792aaf7 thinking slider UI 2025-03-06 02:51:48 -08:00
Andrew Pareles e1a1ca6275 w-full 2025-03-06 02:06:03 -08:00
Andrew Pareles d2e096957e add reasoning slider (still need to hook it up) 2025-03-06 02:01:08 -08:00
Andrew Pareles 3bb9d5b3a5 xAI -> Grok 2025-03-06 02:00:52 -08:00
Andrew Pareles 1be8d17899 add autocomplete UI and filtering 2025-03-06 02:00:52 -08:00
Andrew Pareles 46e8fb1ea5 anthropic thinking output 2025-03-06 02:00:25 -08:00
Andrew Pareles 02d8527244 updated settings for 3.7 2025-03-06 02:00:25 -08:00
Andrew Pareles aeb3f4f97c split model capabilities 2025-03-06 02:00:25 -08:00
Andrew Pareles 46a8e147d0 contrib 2025-03-06 02:00:25 -08:00
Andrew Pareles a5b68448b5 agent 2025-03-06 02:00:25 -08:00
Andrew Pareles c0ea6cc4d8 update contrib 2025-03-06 02:00:24 -08:00
Mathew Pareles 1086b6272a minor change 2025-03-06 00:19:29 -08:00
Mathew Pareles cc6a231b3c fix markdown stylings with prose 2025-03-06 00:09:58 -08:00
Mathew Pareles ead1d9229f fix markdown spacing draft 2025-03-06 00:08:26 -08:00
Mathew Pareles 1b77fc7091 prepare large refactor 2025-03-06 00:07:55 -08:00
Mathew Pareles ee6d5e47ec dropdown + styles 2025-03-06 00:06:14 -08:00
Mathew Pareles a6c9b5f089 edit message bug fixes 2025-03-06 00:06:05 -08:00
Andrew Pareles c9984ce011 Merge pull request #308 from jcommaret/fix-node-version-in-contribution
[Update]node version  in CONTRIBUTING.md
2025-03-05 19:26:57 -08:00
Jérôme Commaret 4eaffd90d1 Update node version in CONTRIBUTING.md 2025-03-06 04:14:02 +01:00
Andrew Pareles 027c0ee6bd the retry loop works! 2025-03-04 05:20:23 -08:00
Andrew Pareles 4ba7352374 fix substr logic + nuanced FINAL_ logic 2025-03-04 03:33:44 -08:00
Andrew Pareles 866547df79 fix extractCodeFromResult Search/Replace 2025-03-04 02:24:25 -08:00
Andrew Pareles 7f278aafcb fix SEARCH/REPLACE detection 2025-03-04 01:18:10 -08:00
Andrew Pareles 5c5a44e4ac stop omitting system message 2025-03-03 23:54:57 -08:00
Andrew Pareles 83f9ff54ab update 2025-03-03 23:35:36 -08:00
Andrew Pareles 1259e71589 update prompt and add accept/reject ui 2025-03-03 23:01:24 -08:00
Andrew Pareles c9cc6cc6a1 properly deal with VSCode chat 2025-03-03 22:46:40 -08:00
Andrew Pareles dc65016ecb clarity 2025-03-03 22:11:21 -08:00
Andrew Pareles 9aae56682e sweep to fix lint 2025-03-03 21:59:47 -08:00
Andrew Pareles 176d302258 lint 2025-03-03 21:53:57 -08:00
Andrew Pareles e23fee5191 fix untraceable import error that breaks everything 2025-03-03 21:53:14 -08:00
Andrew Pareles 4f34af61f3 Merge remote-tracking branch 'origin/fix-css' into model-selection 2025-03-03 21:17:19 -08:00
Andrew Pareles 1257e54ebe tools are getting close 2025-03-03 20:13:26 -08:00
Andrew Pareles d2b5531a33 Update VOID_USEFUL_LINKS.md 2025-03-02 19:46:39 -08:00
Andrew Pareles 993c171e06 Merge pull request #304 from voideditor/model-selection
Fix tailwind
2025-03-01 19:23:32 -08:00
Jérôme Commaret c3c760c03b Merge branch 'voideditor:main' into voidVersion-from-tag 2025-02-12 21:22:07 +01:00
Jérôme Commaret 12c686c102 Bump: voidVersion 2025-02-11 14:43:30 +01:00
Jérôme Commaret 7bceceef85 Bump: voidVersion 2025-02-11 14:21:02 +01:00
Jérôme Commaret ba15b31661 fix 2025-02-11 14:19:38 +01:00
Jérôme Commaret 3b09174587 bad hash and date, will be updated 2025-02-11 14:19:16 +01:00
Jérôme Commaret 7482e2aa82 version : tag, commit, and date from iProductConfiguration 2025-02-11 13:00:23 +01:00
Jérôme Commaret e3c826b253 Bump: voidVersion 2025-02-11 12:56:53 +01:00
Jérôme Commaret db69464ddc Bump: voidVersion 2025-02-11 12:08:53 +01:00
Jérôme Commaret 014c87bc3e Fetch and display : voidVersion into dialog handler 2025-02-11 12:08:03 +01:00
Jérôme Commaret ba606d229a Added voidVersion to IProductConfiguration 2025-02-11 12:07:06 +01:00
Jérôme Commaret fbb4faacbc added voidVersion to product.json 2025-02-11 12:06:24 +01:00
Jérôme Commaret 9b7fd09278 removed voidVersion from package.json 2025-02-11 12:05:47 +01:00
Jérôme Commaret b90973b195 Bump: voidVersion 2025-02-11 11:36:49 +01:00
117 changed files with 11310 additions and 6813 deletions
+1 -1
View File
@@ -1,5 +1,5 @@
disturl="https://electronjs.org/headers"
target="34.2.0"
target="34.3.2"
ms_build_id="11044223"
runtime="electron"
build_from_source="true"
+4 -3
View File
@@ -12,7 +12,7 @@ There are a few ways to contribute:
### Codebase Guide
We highly recommend reading [this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization.
We [highly recommend reading this](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) article on VSCode's sourcecode organization too. Void's codebase is pretty simple when you know what a service is and what `browser/` and `common/` mean, and the article covers all the jargon.
<!-- ADD BLOG HERE
We wrote a [guide to working in VSCode].
@@ -61,13 +61,14 @@ To build Void, open `void/` inside VSCode. Then open your terminal and run:
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.
- This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code).
4. Run Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
6. Nice-to-knows.
- You can always press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload and see your new changes. It's faster than <kbd>Ctrl+Shift+P</kbd> and `Reload Window`.
- You might want to add the flags `--user-data-dir ./.tmp/user-data --extensions-dir ./.tmp/extensions` to the above run command, which lets you delete the `.tmp` folder to reset any IDE changes you made when testing.
- You can kill any of the build scripts by pressing `Ctrl+D` in VSCode terminal. If you press `Ctrl+C` the script will close but will keep running in the background (to open all background scripts, just re-build).
#### Building Void from Terminal
@@ -85,7 +86,7 @@ Alternatively, if you want to build Void from the terminal, instead of pressing
#### Common Fixes
- Make sure you followed the prerequisite steps.
- Make sure you have Node version `20.16.0` (the version in `.nvmrc`)!
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`)!
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
- If you see missing styles, wait a few seconds and then reload.
- If you have any questions, feel free to [submit an issue](https://github.com/voideditor/void/issues/new). You can also refer to VSCode's complete [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute) page.
+2
View File
@@ -2,6 +2,8 @@
The Void team put together this list of links to get up and running with VSCode's sourcecode. We hope it's helpful!
For a complete guide on building Void, see [Contributing](https://github.com/voideditor/void/blob/main/CONTRIBUTING.md).
## 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.
+75 -75
View File
@@ -1,75 +1,75 @@
c0a187acca68906c4a6387e8fabd052cb031ace6132d60a71001d9a0e891958e *chromedriver-v34.2.0-darwin-arm64.zip
fa5a46d752267d8497d375e19079e8b6a8df70c234a79b2d6b48f5862e1a0abc *chromedriver-v34.2.0-darwin-x64.zip
61e03d4fa570976d80f740637f56192b6448a05a73d1fba9717900b29f2b1b4d *chromedriver-v34.2.0-linux-arm64.zip
ec774d9b1a1b828a0db1502a1017fcab1dfed99b1b6b2fd2308dd600a1efa98a *chromedriver-v34.2.0-linux-armv7l.zip
cc15a6e6206485a2d96649ceb60509b9da04fa2811c4824b2e0eb43d1f4b1417 *chromedriver-v34.2.0-linux-x64.zip
9777122f6684180ef375b9b21dcabbc731d8a8befa300d1d47ad954a5b64c1c8 *chromedriver-v34.2.0-mas-arm64.zip
69451fa148b105fec9644646b22ca758a206499574c5816591354835c8056679 *chromedriver-v34.2.0-mas-x64.zip
eb7adc7e720f5e0f1d2c12ecbe886bdc01f2c9aaa3954bd6ebd313750bb18819 *chromedriver-v34.2.0-win32-arm64.zip
cb1973b0c2f5565974d5c2cb51816692f064b6cdc7897fa341d528ba7f9b14bf *chromedriver-v34.2.0-win32-ia32.zip
af5575b4727c3dbe7272016cbbaa8872043f843168a47d86748a50397efb4f77 *chromedriver-v34.2.0-win32-x64.zip
fcc718af2a28fb953290dc971e945818b4dbb293f297e6e25acb669d450cc0dd *electron-api.json
fa47e752e559a6472b87d8907f63296ed8cd53ecf862a4ae113018474d40aa8e *electron-v34.2.0-darwin-arm64-dsym-snapshot.zip
336c3374e721e2379901141be6345459f78d243b037c65b55bac4ae8cb14bfbe *electron-v34.2.0-darwin-arm64-dsym.zip
ac3b9d712d9f036f066d8eba42797117a513e2d250fcc117f0354300b7d5c1e5 *electron-v34.2.0-darwin-arm64-symbols.zip
ee447c17b2ac545e48083113d7e39a916821c1316f60f42cbcbee4fffe7c022a *electron-v34.2.0-darwin-arm64.zip
d7510bc038d06b26690ac9a78fbb256626502303ff7f5b1c2242f64a02832b1b *electron-v34.2.0-darwin-x64-dsym-snapshot.zip
0e95c2bbda00afe78e6229b824e3ffe0cc8612956dc11a5a30380224acdbecae *electron-v34.2.0-darwin-x64-dsym.zip
6d734ef8e8fd007071aae9a13534894dd801f11900f3e73e49cf5352b426e575 *electron-v34.2.0-darwin-x64-symbols.zip
8ef741819c8a5370dabc3b9df5e6ac217366477c5d5c656ed23c800bc984d887 *electron-v34.2.0-darwin-x64.zip
c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-arm64-debug.zip
51e887c382593021127593ceba89ad662d3a6de0f748b94fe3a0ce78a7393923 *electron-v34.2.0-linux-arm64-symbols.zip
818c91309da8ff948c43df58a996c05c0d27daa690e1d659355d9db01e351919 *electron-v34.2.0-linux-arm64.zip
c2f448882a0392ebfd9810058a6a9580b087002c74fca3dcd3cf4ba5c3b27a7c *electron-v34.2.0-linux-armv7l-debug.zip
71ec2b7473db766194bcf04648229c4affedce06e150e692745cc72dbc3749c5 *electron-v34.2.0-linux-armv7l-symbols.zip
0c75996c6bf2d37d0441d3e1021386a9348f8d21783d75571cdb10ede606424f *electron-v34.2.0-linux-armv7l.zip
d1f17be8df8ec6dc1a23afd8a7752f0b949c755a493b8a2c1e83f76c629258ca *electron-v34.2.0-linux-x64-debug.zip
d8fa79154b0b663dbd0054d69f53aec326997449ef21943d27225a2d6899660f *electron-v34.2.0-linux-x64-symbols.zip
f12a02d86cc657500978d263ec6d1841b6e2085cd3bd4d30a97cfe14685396f3 *electron-v34.2.0-linux-x64.zip
71ef8bfebb8513a405fd2beb44ad18f802bbac2248f81a5328ddaaa12906d70c *electron-v34.2.0-mas-arm64-dsym-snapshot.zip
437aca6cad3158f15fd852e5913462637052940f812b20ccc10fccc133d5a0d6 *electron-v34.2.0-mas-arm64-dsym.zip
7f253375a7b43d34b770c03153e47185be7a64bc0c10a783993a93144eb35492 *electron-v34.2.0-mas-arm64-symbols.zip
1f601c20430b036b485c7dc02a39a297f0d08905462a4568306c12f299375e2f *electron-v34.2.0-mas-arm64.zip
d32664181804a17f825bf1fbfd8f0cbe01e0b41b284937359e6d9754b3481ed8 *electron-v34.2.0-mas-x64-dsym-snapshot.zip
f6b394b89fb77dfeefdf525739fe8cd9695f8c2ac2251ed7c571a376cde059e9 *electron-v34.2.0-mas-x64-dsym.zip
539328d93e9bc122e6e34faeab6a42f4845c523f796c7a11bbdcfe5e856f6b98 *electron-v34.2.0-mas-x64-symbols.zip
749b6ced7a9d35a719ad98d4c3bf1a08c315c19ce97d2497235d4ac302cda665 *electron-v34.2.0-mas-x64.zip
e843ea4cb7a93686728d056c957c124760886167932ff619b518e45917edf38a *electron-v34.2.0-win32-arm64-pdb.zip
4908423be5f8ad1b5dd737edfd69ee0870460e16bb639bb963b0981bda2628e4 *electron-v34.2.0-win32-arm64-symbols.zip
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-arm64-toolchain-profile.zip
1629cec7b5620e6ca3b5305c393ae147d1a3871a8f164d686b7bee3810fc1109 *electron-v34.2.0-win32-arm64.zip
e7182f1ef5ed13187fe12f35133cefb50c59e1d52ada758cbb72813dda575205 *electron-v34.2.0-win32-ia32-pdb.zip
a10f778f62bf060a7e38f5ea75ea472679b9ef4f12767ee0703e6d77effae78a *electron-v34.2.0-win32-ia32-symbols.zip
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-ia32-toolchain-profile.zip
96396712a0240f04471f71246b3885a513b08e3f2d40154272d34e59419db7e6 *electron-v34.2.0-win32-ia32.zip
0c55ef5b1a6ea4604e3f0e9f8b5e946944abd1cfc3b974432d40e924a9996812 *electron-v34.2.0-win32-x64-pdb.zip
d6b67cf12edabcc62ae21e7c90ac6b1161dbefe4e6b765c69fc7040096b7d026 *electron-v34.2.0-win32-x64-symbols.zip
9d13b2bd61416eec28f43faa399cc5c0dc9e36dec226314bbf397828f55d74de *electron-v34.2.0-win32-x64-toolchain-profile.zip
a4fdf617dca787b7f1c6f8d0d1cb69c8adb37ee23f9553fe69803f9cad713360 *electron-v34.2.0-win32-x64.zip
5ca44a4ee9cc74a56c9556ce3f3774986dbb8b4f4953088c7f4577bfba4356a8 *electron.d.ts
86247a6815cc98f321374edcbfc0ab7ee79bcb13a5c25213f42151e4059ec690 *ffmpeg-v34.2.0-darwin-arm64.zip
e94f4707a91194f97d0451f9d5a27982c873a5c13d83d20926ce6fdb613f536c *ffmpeg-v34.2.0-darwin-x64.zip
947d7b7cb035eab94cc15b602dfa8a925cf238c1d9225c01732fe0c41f59c571 *ffmpeg-v34.2.0-linux-arm64.zip
64d74b6b94ac4fd755b97e0ec8d50af9cd73810fdb52f6c081b13a3337ace0ea *ffmpeg-v34.2.0-linux-armv7l.zip
a62cb20c5e5f2ba6f1df6f1bc406cc30f7ed44fe6380b506afa333d1b4ad7a87 *ffmpeg-v34.2.0-linux-x64.zip
715bbb3d193b1ca57d4b329edd05cd6b125dc188cdb73d1c66f220f62c8562e3 *ffmpeg-v34.2.0-mas-arm64.zip
866bf47106e8c3e50f046783eb8f5c756069c6de962a46f4edc7a5ee7e4593ee *ffmpeg-v34.2.0-mas-x64.zip
71a2a6c4cca15ccbcbf8912f5d73f855b0ca79804f941f68527ae808f8163957 *ffmpeg-v34.2.0-win32-arm64.zip
399f841cca166781078ca42c8ea060e3d5850ec6cb79716186d0806f3ce20842 *ffmpeg-v34.2.0-win32-ia32.zip
afad2a44f20a0c0c01fb1fea637f3f821842399593c9961c74d650ca12d32cbb *ffmpeg-v34.2.0-win32-x64.zip
c95fdc9dba05aa68aeccb69d4c34f0cb1fb98d7f5291d974d0b638488693655f *hunspell_dictionaries.zip
0abe74138afdb6e45a085d77407659f13c75ab96f694313d4e98bd662f9c6df2 *libcxx-objects-v34.2.0-linux-arm64.zip
3d0cbf6fb150b006428eab78235856d2204d5e93ca85f162e429b4c8bd9b0d3b *libcxx-objects-v34.2.0-linux-armv7l.zip
3a5491e32cec825499919be1b8bf0550d28fe5a31ff00a95572d49a58bb4820a *libcxx-objects-v34.2.0-linux-x64.zip
ff8753d52f759041b8e5462125ee2b96798fe8b5047f8fb8ae60cd07af2fb13d *libcxx_headers.zip
c98cce0091681bc367a48f66c5f4602961aa9cb6dd1a995d8969d6b39ce732f3 *libcxxabi_headers.zip
78a9606190fb227460ddcd276ad540873595d6a82fd1007f42900f53347e3eb1 *mksnapshot-v34.2.0-darwin-arm64.zip
6d4587a36f509356a908b6752de527cfe8a294a2d435b82ea3a98288c3a3a178 *mksnapshot-v34.2.0-darwin-x64.zip
17737bd34f7feefc5143b745f2c4f0e5a41678780e144eb43de41f8d4b9d7852 *mksnapshot-v34.2.0-linux-arm64-x64.zip
565aa9a84d913b7b5eb8d3b868cff151cb8a6c16548233ffd92b2f9bf3789227 *mksnapshot-v34.2.0-linux-armv7l-x64.zip
7332d7864ab4e5ee7fa8d00580d9357f30d0e69d3d1434d67c61f79f666314a4 *mksnapshot-v34.2.0-linux-x64.zip
cc81bfc9894378a9fc8a777429e04cc80860663b3dff8eba1f8cc72559a4f23d *mksnapshot-v34.2.0-mas-arm64.zip
c98230088698638159f6e7e0c5ddde3cb4dba9fa81d76e3c58fc86f96b63ef81 *mksnapshot-v34.2.0-mas-x64.zip
cc7b42943d998e1083ad8119dc2201dd27d709c15aa2a9b78f409485d2927592 *mksnapshot-v34.2.0-win32-arm64-x64.zip
ade9e3126f113318e226f9bdeba068b4383b30a98517a361386eff448459effa *mksnapshot-v34.2.0-win32-ia32.zip
344292ea318dc0e21f37bc7c82d57a57449f0fb278d9868f1b1b597bdcb1f36f *mksnapshot-v34.2.0-win32-x64.zip
c9b82c9f381742e839fea00aeb14f24519bcaf38a0f4eed25532191701f9535b *chromedriver-v34.3.2-darwin-arm64.zip
d556c1e2b06f1bf131e83c2fb981de755c28e1083a884d257eb964815be16b0c *chromedriver-v34.3.2-darwin-x64.zip
1cabad4f3303ac2ff172a9f22185f64944dbaa6fc68271609077158eaefdee35 *chromedriver-v34.3.2-linux-arm64.zip
4213ce52c72ef414179b5c5c22ae8423847ff030d438296bd6c2aac763930a7b *chromedriver-v34.3.2-linux-armv7l.zip
3c64c08221fdfc0f4be60ea8b1b126f2ecca45f60001b63778522f711022c6ea *chromedriver-v34.3.2-linux-x64.zip
e8388734d88e011cb6cd79795431de9206820749219d80565ee49d90501d2bf3 *chromedriver-v34.3.2-mas-arm64.zip
3ad1dd37bd6e0bb37e8503898db7aedd56bd5213e6d6760b05c3d11f4625062b *chromedriver-v34.3.2-mas-x64.zip
d567b481a0f5d88e84bba7718f89fb08f56363bfc4cb5914e1c2086358a5c252 *chromedriver-v34.3.2-win32-arm64.zip
df6732e9dc61cb20a3c0b2a2de453aac7e2bd54e7cbff43512afa614852c15fa *chromedriver-v34.3.2-win32-ia32.zip
dda0765c8d064924632e18cd152014ecd767f3808fc51c8249a053bfb7ca70a2 *chromedriver-v34.3.2-win32-x64.zip
1945f15caff98f2e0f1ee539c483d352fb8d4d0c13f342caa7abe247676d828c *electron-api.json
c078bbf727b3c3026f60e07a0f4643b85c06c581b54be017d0a6c284ba6772d3 *electron-v34.3.2-darwin-arm64-dsym-snapshot.zip
35f587754d6a3272606258386bf73688d63dd53c7e572d3a7cbaae6f3f60bdae *electron-v34.3.2-darwin-arm64-dsym.zip
08b14ee02c98353de3c738120dfd017322666e82b914a7f6de9b9888dcc5c0f0 *electron-v34.3.2-darwin-arm64-symbols.zip
2a4aa7e8fa30f229e465ebd18d3e4722e2b41529dc51a68a954d333a7e556ffe *electron-v34.3.2-darwin-arm64.zip
1509ccdeb80024f5e3edd5ecf804b4cef4e47ea2bd74e33ef0b39044b0ccf892 *electron-v34.3.2-darwin-x64-dsym-snapshot.zip
3bbe5d587c3f582ed8c126b0fb635cc02ad9a14d077b04892fe6f862092445b0 *electron-v34.3.2-darwin-x64-dsym.zip
fa7ece82e6ecaf1c94ed341e8ebff98e64687c68fe113f52cd9a21400302e22f *electron-v34.3.2-darwin-x64-symbols.zip
23938c62257a65a863ed7aa7c7966ba5f257a7d3dc16c78293e826962cc39c5c *electron-v34.3.2-darwin-x64.zip
0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-arm64-debug.zip
676d0dc2b1c1c85c8b2abbb8cd5376ee22ecdb910493b910d9ae5a998532136a *electron-v34.3.2-linux-arm64-symbols.zip
774e4ccb39d553e5487994a9f8c60774a90f08cdb049ff65f3963fc27c969ff2 *electron-v34.3.2-linux-arm64.zip
0547eecf8ab538d74fa854f591ce8b888a3dbb339256d2db3038e7bb2c6dd929 *electron-v34.3.2-linux-armv7l-debug.zip
ba33bf53fcb35dea568a2795f5b23ecf46c218abe8258946611c72a1f42f716c *electron-v34.3.2-linux-armv7l-symbols.zip
73ae92c8fffb351d3a455569cf57ce9a3f676f42bf61939c613c607fe0fc3bfb *electron-v34.3.2-linux-armv7l.zip
e61a9a69dd7ea6f2687708a8e83516670cdea53c728226e598e2f6f1fad5b77b *electron-v34.3.2-linux-x64-debug.zip
f1a04df7fe67dd1cd29e7b87871525458d2eb24c0cf3b5835a1c56974707562a *electron-v34.3.2-linux-x64-symbols.zip
7b74c0c4fae82e27c7e9cbca13e9763e046113dba8737d3e27de9a0b300ac87e *electron-v34.3.2-linux-x64.zip
8571a6aa83e00925ceb39fdc5a45a9f6b9aa3d92fd84951c6f252ed723aea4ae *electron-v34.3.2-mas-arm64-dsym-snapshot.zip
477410c6f9a6c5eeaedf376058a02c2996fc0a334aa40eeec7d3734c09347f4d *electron-v34.3.2-mas-arm64-dsym.zip
c2e62dcd6630cb51b2d8e2869e74e47d29bda785521cea6e82e546d0fc58aabb *electron-v34.3.2-mas-arm64-symbols.zip
a1698e8546a062fd59b7f8e5507a7f3220fb00b347f2377de83fc9a07f7f3507 *electron-v34.3.2-mas-arm64.zip
741a24ac230a3651dca81d211f9f00b835c428a5ed0c5f67d370d4e88b62f8d6 *electron-v34.3.2-mas-x64-dsym-snapshot.zip
aeff97ec9e5c9e173ac89e38acd94476025c5640d5f27be1e8c2abd54398bab3 *electron-v34.3.2-mas-x64-dsym.zip
9f14b66b1d612ac66697288e8763171c388f7f200854871a5f0ab464a6a921c2 *electron-v34.3.2-mas-x64-symbols.zip
c979d7e7175f1e8e03ca187997d4c156b878189fc3611b347fadebcb16f3e027 *electron-v34.3.2-mas-x64.zip
f43c700641e8220205dd356952e32718d113cf530520c4ed7209b59851eac266 *electron-v34.3.2-win32-arm64-pdb.zip
3ba6e01c99bffac6b5dd3fd6f122ecdb571cf6f675dc5498c65050bd7a382ef8 *electron-v34.3.2-win32-arm64-symbols.zip
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-arm64-toolchain-profile.zip
9b9cb65d75a16782088b492f9ef3bb4d27525012b819c12bf29bd27e159d749b *electron-v34.3.2-win32-arm64.zip
1006e7af4c149114b5ebc3497617aaa6cd1bb0b131e0a225fd73709ff308f9c5 *electron-v34.3.2-win32-ia32-pdb.zip
1ecb6430cd04454f08f557c9579163f3552144bfcc0b67b768dad8868b5b891d *electron-v34.3.2-win32-ia32-symbols.zip
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-ia32-toolchain-profile.zip
d004fd5f853754001fafaec33e383d1950b30c935ee71b297ec1c9e084355e9b *electron-v34.3.2-win32-ia32.zip
4e0721552fd2f09e9466e88089af8b965f1bfbc4ae00a59aaf6245b1d1efabfd *electron-v34.3.2-win32-x64-pdb.zip
9dea812a7e7cd0fb18e5fed9a99db5531959a068c24d3c0ecedceb644cd3ffa0 *electron-v34.3.2-win32-x64-symbols.zip
c23f84aabb09c24cd2ae759a547fdba4206af19a3bb0f4554a91cd9528648ad0 *electron-v34.3.2-win32-x64-toolchain-profile.zip
1785e161420fb90d2331c26e50bba3413cae9625b7db3c8524ea02ade631efba *electron-v34.3.2-win32-x64.zip
722b304c31ddac58b0083d94a241c5276464f04bd8ea4fcbfd33051d197be103 *electron.d.ts
31ce159b2e47d1de5bc907d8e1c89726b0f2ba530ec2e9d7a8e5c723b1ccf6e0 *ffmpeg-v34.3.2-darwin-arm64.zip
565539bac64a6ee9cf6f188070f520210a1507341718f5dc388ac7c454b1e1d5 *ffmpeg-v34.3.2-darwin-x64.zip
6006ea0f46ab229feb2685be086b0fafd65981e2939dd2218a078459c75ab527 *ffmpeg-v34.3.2-linux-arm64.zip
9404ce2e85df7c40f809f2cf62c7af607de299839fe6b7ae978c3015500abcc8 *ffmpeg-v34.3.2-linux-armv7l.zip
79aec96898b7e2462826780ee0b52b9ab299dc662af333e128a34fd5ddae87f1 *ffmpeg-v34.3.2-linux-x64.zip
9190743c78210574faf5d5ecb82a00f8fa15e5f2253378cb925a99ca9d39961b *ffmpeg-v34.3.2-mas-arm64.zip
48915adcb1a6342efeda896035101300f0432c0304cfb38f2378e98c6309ebae *ffmpeg-v34.3.2-mas-x64.zip
745d5ef786de6d4a720475079836e2fda7b501cfcd255819485a47de5b24b74e *ffmpeg-v34.3.2-win32-arm64.zip
d0d86d60978439dc8ae4a723d4e4c1f853891d596bfd84033440a232fa762e2f *ffmpeg-v34.3.2-win32-ia32.zip
4441539fd8c9cbe79880ff1bade9bdc0c3744c33d7409130af6404e57ee401ff *ffmpeg-v34.3.2-win32-x64.zip
39edd1eeefe881aa75af0e438204e0b1c6e6724e34fa5819109276331c0c2c9a *hunspell_dictionaries.zip
20dd417536e5f4ebc01f480221284c0673729c27b082bc04e2922f16cd571719 *libcxx-objects-v34.3.2-linux-arm64.zip
7e53c5779c04f895f8282c0450ec4a63074d15a0e910e41006cfea133d0288af *libcxx-objects-v34.3.2-linux-armv7l.zip
92e2283c924ab523ffec3ea22513beaab6417f7fc3d570f57d51a1e1ceb7f510 *libcxx-objects-v34.3.2-linux-x64.zip
9bf3c6e8ad68f523fe086fada4148dd04e5bb0b9290d104873e66f2584a5cf50 *libcxx_headers.zip
34e4b44f9c5e08b557a2caed55456ce7690abab910196a783a2a47b58d2b9ac9 *libcxxabi_headers.zip
11f67635e6659f9188198e4086c51b89890b61a22f6c17c99eff35595ee8f51d *mksnapshot-v34.3.2-darwin-arm64.zip
c0add9ef4ac27c73fa043d04b4c9635fd3fd9f5c16d7a03e961864ba05252813 *mksnapshot-v34.3.2-darwin-x64.zip
6262adf86a340d8d28059937b01ef1117b93212e945fddbceea5c18d7e7c64f0 *mksnapshot-v34.3.2-linux-arm64-x64.zip
f7db8ebe91a1cc8d24ef6aad12949a18d8e4975ac296e3e5e9ecd88c9bccb143 *mksnapshot-v34.3.2-linux-armv7l-x64.zip
6642038e86bda362980ff1c8973a404e2b02efdd87de9e35b650fc1e743833da *mksnapshot-v34.3.2-linux-x64.zip
15883bf8e8cd737c3682d1e719d7cbac92f50b525681aac324dca876861dfc7d *mksnapshot-v34.3.2-mas-arm64.zip
4da23a950bfcc377ef21c37d496017ab4c36da03f3b41049ac114042c42608ce *mksnapshot-v34.3.2-mas-x64.zip
fab59573d3c2f9bdf31146a1896d24ac0c51f736aad86d2f3c7ecef13c05a7fd *mksnapshot-v34.3.2-win32-arm64-x64.zip
66f25e07c6f8d5d2009577a129440255a3baf63c929a5b60b2e77cd52e46105b *mksnapshot-v34.3.2-win32-ia32.zip
8168bfbf61882cfac80aed1e71e364e1c7f2fccd11eac298e6abade8b46894ea *mksnapshot-v34.3.2-win32-x64.zip
+3 -1
View File
@@ -54,9 +54,11 @@ async function main(buildDir) {
...defaultOpts,
// TODO(deepak1556): Incorrectly declared type in electron-osx-sign
ignore: (filePath) => {
const ext = path_1.default.extname(filePath);
return filePath.includes(gpuHelperAppName) ||
filePath.includes(rendererHelperAppName) ||
filePath.includes(pluginHelperAppName);
filePath.includes(pluginHelperAppName) ||
ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2';
}
};
const gpuHelperOpts = {
+3 -1
View File
@@ -58,9 +58,11 @@ async function main(buildDir?: string): Promise<void> {
...defaultOpts,
// TODO(deepak1556): Incorrectly declared type in electron-osx-sign
ignore: (filePath: string) => {
const ext = path.extname(filePath);
return filePath.includes(gpuHelperAppName) ||
filePath.includes(rendererHelperAppName) ||
filePath.includes(pluginHelperAppName);
filePath.includes(pluginHelperAppName) ||
ext === '.asar' || ext === '.dat' || ext === '.gif' || ext === '.icns' || ext === '.ico' || ext === '.json' || ext === '.mp3' || ext === '.nib' || ext === '.pak' || ext === '.png' || ext === '.scpt' || ext === '.ttf' || ext === '.wasm' || ext === '.woff' || ext === '.woff2';
}
};
+16 -4
View File
@@ -154,9 +154,20 @@ function getNodeChecksum(expectedName) {
}
function extractAlpinefromDocker(nodeVersion, platform, arch) {
const imageName = arch === 'arm64' ? 'arm64v8/node' : 'node';
let imageName = 'node';
let dockerPlatform = '';
if (arch === 'arm64') {
imageName = 'arm64v8/node';
const architecture = cp.execSync(`docker info --format '{{json .Architecture}}'`, { encoding: 'utf8' }).trim();
if (architecture !== '"aarch64"') {
dockerPlatform = '--platform=linux/arm64';
}
}
log(`Downloading node.js ${nodeVersion} ${platform} ${arch} from docker image ${imageName}`);
const contents = cp.execSync(`docker run --rm ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' });
const contents = cp.execSync(`docker run --rm ${dockerPlatform} ${imageName}:${nodeVersion}-alpine /bin/sh -c 'cat \`which node\`'`, { maxBuffer: 100 * 1024 * 1024, encoding: 'buffer' });
return es.readArray([new File({ path: 'node', contents, stat: { mode: parseInt('755', 8) } })]);
}
@@ -308,10 +319,11 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa
}
const name = product.nameShort;
const release = packageJson.release;
let packageJsonContents;
const packageJsonStream = gulp.src(['remote/package.json'], { base: 'remote' })
.pipe(json({ name, version, dependencies: undefined, optionalDependencies: undefined, type: 'module' }))
.pipe(json({ name, version, release, dependencies: undefined, optionalDependencies: undefined, type: 'module' }))
.pipe(es.through(function (file) {
packageJsonContents = file.contents.toString();
this.emit('data', file);
@@ -319,7 +331,7 @@ function packageTask(type, platform, arch, sourceFolderName, destinationFolderNa
let productJsonContents;
const productJsonStream = gulp.src(['product.json'], { base: '.' })
.pipe(json({ commit, date: readISODate('out-build'), version }))
.pipe(json({ commit, date: readISODate('out-build'), version, release }))
.pipe(es.through(function (file) {
productJsonContents = file.contents.toString();
this.emit('data', file);
+4 -3
View File
@@ -267,7 +267,8 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
}
const name = product.nameShort;
const packageJsonUpdates = { name, version };
const release = packageJson.release;
const packageJsonUpdates = { name, version, release };
if (platform === 'linux') {
packageJsonUpdates.desktopName = `${product.applicationName}.desktop`;
@@ -284,7 +285,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
// 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 }))
.pipe(json({ commit, date: readISODate('out-build'), checksums, version, release }))
.pipe(es.through(function (file) {
productJsonContents = file.contents.toString();
this.emit('data', file);
@@ -372,7 +373,7 @@ function packageTask(platform, arch, sourceFolderName, destinationFolderName, op
} 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/' + product.applicationName));
all = es.merge(all, shortcut);
}
+3 -5
View File
@@ -25,7 +25,6 @@ const exec = util.promisify(cp.exec);
const root = path.dirname(__dirname);
const commit = getVersion(root);
const linuxPackageRevision = Math.floor(new Date().getTime() / 1000);
/**
* @param {string} arch
@@ -87,7 +86,7 @@ function prepareDebPackage(arch) {
const dependencies = await dependenciesGenerator.getDependencies('deb', binaryDir, product.applicationName, debArch);
gulp.src('resources/linux/debian/control.template', { base: '.' })
.pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@VERSION@@', packageJson.version + '-' + linuxPackageRevision))
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
.pipe(replace('@@ARCHITECTURE@@', debArch))
.pipe(replace('@@DEPENDS@@', dependencies.join(', ')))
.pipe(replace('@@RECOMMENDS@@', debianRecommendedDependencies.join(', ')))
@@ -202,8 +201,7 @@ function prepareRpmPackage(arch) {
.pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@NAME_LONG@@', product.nameLong))
.pipe(replace('@@ICON@@', product.linuxIconName))
.pipe(replace('@@VERSION@@', packageJson.version))
.pipe(replace('@@RELEASE@@', linuxPackageRevision))
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
.pipe(replace('@@ARCHITECTURE@@', rpmArch))
.pipe(replace('@@LICENSE@@', product.licenseName))
.pipe(replace('@@QUALITY@@', product.quality || '@@QUALITY@@'))
@@ -279,7 +277,7 @@ function prepareSnapPackage(arch) {
const snapcraft = gulp.src('resources/linux/snap/snapcraft.yaml', { base: '.' })
.pipe(replace('@@NAME@@', product.applicationName))
.pipe(replace('@@VERSION@@', commit.substr(0, 8)))
.pipe(replace('@@VERSION@@', `${packageJson.version}.${packageJson.release}`))
// Possible run-on values https://snapcraft.io/docs/architectures
.pipe(replace('@@ARCHITECTURE@@', arch === 'x64' ? 'amd64' : arch))
.pipe(rename('snap/snapcraft.yaml'));
+2 -2
View File
@@ -87,8 +87,8 @@ function buildWin32Setup(arch, target) {
NameLong: product.nameLong,
NameShort: product.nameShort,
DirName: product.win32DirName,
Version: pkg.version,
RawVersion: pkg.version.replace(/-\w+$/, ''),
Version: `${pkg.version}.${pkg.release}`,
RawVersion: `${pkg.version.replace(/-\w+$/, '')}.${pkg.release}`,
NameVersion: product.win32NameVersion + (target === 'user' ? ' (User)' : ''),
ExeBasename: product.nameShort,
RegValueName: product.win32RegValueName,
+1 -4
View File
@@ -85,10 +85,7 @@ function getExtensionDownloadStream(extension) {
if (extension.vsix) {
input = ext.fromVsix(path_1.default.join(root, extension.vsix), extension);
}
else if (productjson.extensionsGallery?.serviceUrl) {
input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension);
}
else {
else { // Void - ext-from-gh.patch
input = ext.fromGithub(extension);
}
return input.pipe((0, gulp_rename_1.default)(p => p.dirname = `${extension.name}/${p.dirname}`));
+1 -3
View File
@@ -73,9 +73,7 @@ function getExtensionDownloadStream(extension: IExtensionDefinition) {
if (extension.vsix) {
input = ext.fromVsix(path.join(root, extension.vsix), extension);
} else if (productjson.extensionsGallery?.serviceUrl) {
input = ext.fromMarketplace(productjson.extensionsGallery.serviceUrl, extension);
} else {
} else { // Void - ext-from-gh.patch
input = ext.fromGithub(extension);
}
+1 -1
View File
@@ -37,7 +37,7 @@ function getMangledFileContents(projectPath) {
* @type {webpack.LoaderDefinitionFunction}
*/
module.exports = async function (source, sourceMap, meta) {
if (this.mode !== 'production') {
if (true) { // Void - extensions-disable-mangler
// Only enable mangling in production builds
return source;
}
+1 -1
View File
@@ -71,7 +71,7 @@
"type": "string",
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
"scope": "application",
"default": "https://github.com/voideditor/${NAME_OF_REPO}/releases/download/${version}.${release}/void-server-${os}-${arch}-${version}.${release}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
},
"remote.SSH.remotePlatform": {
"type": "object",
@@ -32,12 +32,14 @@ export async function getVSCodeServerConfig(): Promise<IServerConfig> {
const customServerBinaryName = vscode.workspace.getConfiguration('remote.SSH.experimental').get<string>('serverBinaryName', '');
return {
version: vscode.version.replace('-insider', ''),
// version: vscode.version.replace('-insider', ''),
commit: productJson.commit,
quality: productJson.quality,
release: productJson.release,
serverApplicationName: customServerBinaryName || productJson.serverApplicationName,
serverDataFolderName: productJson.serverDataFolderName,
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate
serverDownloadUrlTemplate: productJson.serverDownloadUrlTemplate,
// Void changed this
version: productJson.voidVersion
};
}
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/void-updates-server/releases/download/test/void-server-${os}-${arch}.tar.gz';
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
let shell = 'powershell';
+63 -11
View File
@@ -75,6 +75,7 @@
"devDependencies": {
"@playwright/test": "^1.50.0",
"@stylistic/eslint-plugin-ts": "^2.8.0",
"@tailwindcss/typography": "^0.5.16",
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
"@types/diff": "^7.0.1",
@@ -116,7 +117,7 @@
"cssnano": "^6.0.3",
"debounce": "^1.0.0",
"deemon": "^1.8.0",
"electron": "34.2.0",
"electron": "^34.3.2",
"eslint": "^9.11.1",
"eslint-formatter-compact": "^8.40.0",
"eslint-plugin-header": "3.1.1",
@@ -168,7 +169,7 @@
"pump": "^1.0.1",
"rcedit": "^1.1.0",
"rimraf": "^2.7.1",
"scope-tailwind": "^1.0.6",
"scope-tailwind": "^1.0.9",
"sinon": "^12.0.1",
"sinon-test": "^3.1.3",
"source-map": "0.6.1",
@@ -3586,6 +3587,13 @@
"tslib": "^2.8.0"
}
},
"node_modules/@swc/helpers/node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"dev": true,
"license": "0BSD"
},
"node_modules/@szmarczak/http-timer": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/@szmarczak/http-timer/-/http-timer-4.0.6.tgz",
@@ -3598,6 +3606,36 @@
"node": ">=10"
}
},
"node_modules/@tailwindcss/typography": {
"version": "0.5.16",
"resolved": "https://registry.npmjs.org/@tailwindcss/typography/-/typography-0.5.16.tgz",
"integrity": "sha512-0wDLwCVF5V3x3b1SGXPCDcdsbDHMBe+lkFzBRaHeLvNi+nrrnZ1lA18u+OTWO8iSWU2GxUOCvlXtDuqftc1oiA==",
"dev": true,
"license": "MIT",
"dependencies": {
"lodash.castarray": "^4.4.0",
"lodash.isplainobject": "^4.0.6",
"lodash.merge": "^4.6.2",
"postcss-selector-parser": "6.0.10"
},
"peerDependencies": {
"tailwindcss": ">=3.0.0 || insiders || >=4.0.0-alpha.20 || >=4.0.0-beta.1"
}
},
"node_modules/@tailwindcss/typography/node_modules/postcss-selector-parser": {
"version": "6.0.10",
"resolved": "https://registry.npmjs.org/postcss-selector-parser/-/postcss-selector-parser-6.0.10.tgz",
"integrity": "sha512-IQ7TZdoaqbT+LCpShg46jnZVlhWD2w6iQYAcYXfHARZ7X1t/UGhhceQDs5X0cGqKvYlHNOuv7Oa1xmb0oQuA3w==",
"dev": true,
"license": "MIT",
"dependencies": {
"cssesc": "^3.0.0",
"util-deprecate": "^1.0.2"
},
"engines": {
"node": ">=4"
}
},
"node_modules/@thisismanta/pessimist": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/@thisismanta/pessimist/-/pessimist-1.2.0.tgz",
@@ -7970,9 +8008,9 @@
"dev": true
},
"node_modules/electron": {
"version": "34.2.0",
"resolved": "https://registry.npmjs.org/electron/-/electron-34.2.0.tgz",
"integrity": "sha512-SYwBJNeXBTm1q/ErybQMUBZAYqEreBUqBwTrNkw1rV4YatDZk5Aittpcus3PPeC4UoI/tqmJ946uG8AKHTd6CA==",
"version": "34.3.2",
"resolved": "https://registry.npmjs.org/electron/-/electron-34.3.2.tgz",
"integrity": "sha512-n9tzmFexVLxipZXwMTY30H10f0X9k2OP0SkpSwL5VvnDZi0l/Hc+8CEArKkQPbbSf/IS7nxgc96gtTaR+XoSBg==",
"dev": true,
"hasInstallScript": true,
"license": "MIT",
@@ -14039,6 +14077,13 @@
"integrity": "sha1-soqmKIorn8ZRA1x3EfZathkDMaY= sha512-TwuEnCnxbc3rAvhf/LbG7tJUDzhqXyFnv3dtzLOPgCG/hODL7WFnsbwktkD7yUV0RrreP/l1PALq/YSg6VvjlA==",
"dev": true
},
"node_modules/lodash.castarray": {
"version": "4.4.0",
"resolved": "https://registry.npmjs.org/lodash.castarray/-/lodash.castarray-4.4.0.tgz",
"integrity": "sha512-aVx8ztPv7/2ULbArGJ2Y42bG1mEQ5mGjpdvrbJcJFU3TbYybe+QlLS4pst9zV52ymy2in1KpFPiZnAOATxD4+Q==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.clone": {
"version": "4.5.0",
"resolved": "https://registry.npmjs.org/lodash.clone/-/lodash.clone-4.5.0.tgz",
@@ -14057,6 +14102,13 @@
"integrity": "sha1-LRd/ZS+jHpObRDjVNBSZ36OCXpk= sha512-z+Uw/vLuy6gQe8cfaFWD7p0wVv8fJl3mbzXh33RS+0oW2wvUqiRXiQ69gLWSLpgB5/6sU+r6BlQR0MBILadqTQ==",
"dev": true
},
"node_modules/lodash.isplainobject": {
"version": "4.0.6",
"resolved": "https://registry.npmjs.org/lodash.isplainobject/-/lodash.isplainobject-4.0.6.tgz",
"integrity": "sha512-oSXzaWypCMHkPC3NvBEaPHf0KsA5mvPrOPgQWDsbg8n7orZ290M0BmC/jgRZ4vcJ6DTAhjrsSYgdsW/F+MFOBA==",
"dev": true,
"license": "MIT"
},
"node_modules/lodash.memoize": {
"version": "4.1.2",
"resolved": "https://registry.npmjs.org/lodash.memoize/-/lodash.memoize-4.1.2.tgz",
@@ -18820,9 +18872,9 @@
"dev": true
},
"node_modules/scope-tailwind": {
"version": "1.0.6",
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.6.tgz",
"integrity": "sha512-tkISLsaesYKKXL9YrLsRWFOD/FhrRVGKeinjgTuFtEidryLzwlBB3G17ArmHWHYcfdMp00XwnRMcGFkF8wwG6w==",
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.9.tgz",
"integrity": "sha512-sxtAKxJq143lYK/RCE36YGq13ficBZ9/9Z0TZa78k0AEiKNT5nH4kfhD8YAfEXR/qPR+G7tl9KL4UoHh+Cs93g==",
"dev": true,
"license": "AGPL-3.0-only",
"dependencies": {
@@ -21398,9 +21450,9 @@
}
},
"node_modules/tslib": {
"version": "2.8.1",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
"version": "2.6.3",
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.3.tgz",
"integrity": "sha512-xNvxJEOUiWPGhUuUdQgAJPKOOJfGnIyKySOc09XkKsgdUV/3E2zvwZYdejjmRgPCgcym1juLH3226yA7sEFJKQ==",
"dev": true,
"license": "0BSD"
},
+3 -2
View File
@@ -136,6 +136,7 @@
"devDependencies": {
"@playwright/test": "^1.50.0",
"@stylistic/eslint-plugin-ts": "^2.8.0",
"@tailwindcss/typography": "^0.5.16",
"@types/cookie": "^0.3.3",
"@types/debug": "^4.1.5",
"@types/diff": "^7.0.1",
@@ -177,7 +178,7 @@
"cssnano": "^6.0.3",
"debounce": "^1.0.0",
"deemon": "^1.8.0",
"electron": "34.2.0",
"electron": "34.3.2",
"eslint": "^9.11.1",
"eslint-formatter-compact": "^8.40.0",
"eslint-plugin-header": "3.1.1",
@@ -229,7 +230,7 @@
"pump": "^1.0.1",
"rcedit": "^1.1.0",
"rimraf": "^2.7.1",
"scope-tailwind": "^1.0.6",
"scope-tailwind": "^1.0.9",
"sinon": "^12.0.1",
"sinon-test": "^3.1.3",
"source-map": "0.6.1",
+6 -1
View File
@@ -1,6 +1,7 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.0.4",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
@@ -34,5 +35,9 @@
"serviceUrl": "https://marketplace.visualstudio.com/_apis/public/gallery",
"itemUrl": "https://marketplace.visualstudio.com/items"
},
"builtInExtensions": []
"builtInExtensions": [],
"linkProtectionTrustedDomains": [
"https://voideditor.com",
"https://voideditor.dev"
]
}
+1231 -1039
View File
File diff suppressed because it is too large Load Diff
Binary file not shown.

Before

Width:  |  Height:  |  Size: 173 KiB

After

Width:  |  Height:  |  Size: 200 KiB

+36 -3
View File
@@ -107,13 +107,46 @@ find . -maxdepth 1 ! -name VoidApp.AppDir ! -name "." ! -name ".." -exec cp -r {
cp void.png VoidApp.AppDir/ && \
echo "[Desktop Entry]" > VoidApp.AppDir/void.desktop && \
echo "Name=Void" >> VoidApp.AppDir/void.desktop && \
echo "Exec=void" >> VoidApp.AppDir/void.desktop && \
echo "Comment=Open source AI code editor." >> VoidApp.AppDir/void.desktop && \
echo "GenericName=Text Editor" >> VoidApp.AppDir/void.desktop && \
echo "Exec=void %F" >> VoidApp.AppDir/void.desktop && \
echo "Icon=void" >> VoidApp.AppDir/void.desktop && \
echo "Type=Application" >> VoidApp.AppDir/void.desktop && \
echo "Categories=Utility;" >> VoidApp.AppDir/void.desktop && \
echo "Comment=Void Linux Application" >> VoidApp.AppDir/void.desktop && \
echo "StartupNotify=false" >> VoidApp.AppDir/void.desktop && \
echo "StartupWMClass=Void" >> VoidApp.AppDir/void.desktop && \
echo "Categories=TextEditor;Development;IDE;" >> VoidApp.AppDir/void.desktop && \
echo "MimeType=application/x-void-workspace;" >> VoidApp.AppDir/void.desktop && \
echo "Keywords=void;" >> VoidApp.AppDir/void.desktop && \
echo "Actions=new-empty-window;" >> VoidApp.AppDir/void.desktop && \
echo "[Desktop Action new-empty-window]" >> VoidApp.AppDir/void.desktop && \
echo "Name=New Empty Window" >> VoidApp.AppDir/void.desktop && \
echo "Name[de]=Neues leeres Fenster" >> VoidApp.AppDir/void.desktop && \
echo "Name[es]=Nueva ventana vacía" >> VoidApp.AppDir/void.desktop && \
echo "Name[fr]=Nouvelle fenêtre vide" >> VoidApp.AppDir/void.desktop && \
echo "Name[it]=Nuova finestra vuota" >> VoidApp.AppDir/void.desktop && \
echo "Name[ja]=新しい空のウィンドウ" >> VoidApp.AppDir/void.desktop && \
echo "Name[ko]=새 빈 창" >> VoidApp.AppDir/void.desktop && \
echo "Name[ru]=Новое пустое окно" >> VoidApp.AppDir/void.desktop && \
echo "Name[zh_CN]=新建空窗口" >> VoidApp.AppDir/void.desktop && \
echo "Name[zh_TW]=開新空視窗" >> VoidApp.AppDir/void.desktop && \
echo "Exec=void --new-window %F" >> VoidApp.AppDir/void.desktop && \
echo "Icon=void" >> VoidApp.AppDir/void.desktop && \
chmod +x VoidApp.AppDir/void.desktop && \
cp VoidApp.AppDir/void.desktop VoidApp.AppDir/usr/share/applications/ && \
echo "[Desktop Entry]" > VoidApp.AppDir/void-url-handler.desktop && \
echo "Name=Void - URL Handler" > VoidApp.AppDir/void-url-handler.desktop && \
echo "Comment=Open source AI code editor." > VoidApp.AppDir/void-url-handler.desktop && \
echo "GenericName=Text Editor" > VoidApp.AppDir/void-url-handler.desktop && \
echo "Exec=void --open-url %U" > VoidApp.AppDir/void-url-handler.desktop && \
echo "Icon=void" > VoidApp.AppDir/void-url-handler.desktop && \
echo "Type=Application" > VoidApp.AppDir/void-url-handler.desktop && \
echo "NoDisplay=true" > VoidApp.AppDir/void-url-handler.desktop && \
echo "StartupNotify=true" > VoidApp.AppDir/void-url-handler.desktop && \
echo "Categories=Utility;TextEditor;Development;IDE;" > VoidApp.AppDir/void-url-handler.desktop && \
echo "MimeType=x-scheme-handler/void;" > VoidApp.AppDir/void-url-handler.desktop && \
echo "Keywords=void;" > VoidApp.AppDir/void-url-handler.desktop && \
chmod +x VoidApp.AppDir/void-url-handler.desktop && \
cp VoidApp.AppDir/void-url-handler.desktop VoidApp.AppDir/usr/share/applications/ && \
echo "#!/bin/bash" > VoidApp.AppDir/AppRun && \
echo "HERE=\$(dirname \"\$(readlink -f \"\${0}\")\")" >> VoidApp.AppDir/AppRun && \
echo "export PATH=\${HERE}/usr/bin:\${PATH}" >> VoidApp.AppDir/AppRun && \
+12
View File
@@ -0,0 +1,12 @@
[Desktop Entry]
Name=Void - URL Handler
Comment=Open source AI code editor.
GenericName=Text Editor
Exec=void --open-url %U
Icon=void
Type=Application
NoDisplay=true
StartupNotify=true
Categories=Utility;TextEditor;Development;IDE;
MimeType=x-scheme-handler/void;
Keywords=void;
+24 -4
View File
@@ -1,7 +1,27 @@
[Desktop Entry]
Name=void
Exec=void
Name=Void
Comment=Open source AI code editor.
GenericName=Text Editor
Exec=void %F
Icon=void
Type=Application
Categories=Utility;
Comment=Void Linux Application
StartupNotify=false
StartupWMClass=Void
Categories=TextEditor;Development;IDE;
MimeType=application/x-void-workspace;
Keywords=void;
Actions=new-empty-window;
[Desktop Action new-empty-window]
Name=New Empty Window
Name[de]=Neues leeres Fenster
Name[es]=Nueva ventana vacía
Name[fr]=Nouvelle fenêtre vide
Name[it]=Nuova finestra vuota
Name[ja]=新しい空のウィンドウ
Name[ko]=새 빈 창
Name[ru]=Новое пустое окно
Name[zh_CN]=新建空窗口
Name[zh_TW]=開新空視窗
Exec=void --new-window %F
Icon=void
+14
View File
@@ -6,6 +6,7 @@
import * as path from 'path';
import * as fs from 'original-fs';
import * as os from 'os';
import { createRequire } from 'node:module';
import { performance } from 'perf_hooks';
import { configurePortable } from './bootstrap-node.js';
import { bootstrapESM } from './bootstrap-esm.js';
@@ -22,6 +23,7 @@ import { INLSConfiguration } from './vs/nls.js';
import { NativeParsedArgs } from './vs/platform/environment/common/argv.js';
const __dirname = path.dirname(fileURLToPath(import.meta.url));
const require = createRequire(import.meta.url);
perf.mark('code/didStartMain');
@@ -110,6 +112,17 @@ protocol.registerSchemesAsPrivileged([
// Global app listeners
registerListeners();
function resolveUserProduct() {
const userProductPath = path.join(userDataPath, 'product.json');
try {
// Assign the product configuration to the global scope
const productJson = require(userProductPath);
// @ts-expect-error
globalThis._VSCODE_USER_PRODUCT_JSON = productJson;
} catch (ex) {
}
}
/**
* We can resolve the NLS configuration early if it is defined
* in argv.json before `app.ready` event. Otherwise we can only
@@ -206,6 +219,7 @@ async function onReady() {
async function startup(codeCachePath: string | undefined, nlsConfig: INLSConfiguration): Promise<void> {
process.env['VSCODE_NLS_CONFIG'] = JSON.stringify(nlsConfig);
process.env['VSCODE_CODE_CACHE_PATH'] = codeCachePath || '';
resolveUserProduct();
// Bootstrap ESM
await bootstrapESM();
@@ -26,7 +26,7 @@
}
/* custom speed & easing for loading icon */
.codicon-loading,
.codicon-loading:not(.codicon-no-default-spin), /* Void changed this as it is literally broken to the !important */
.codicon-tree-item-loading::before {
animation-duration: 1s !important;
animation-timing-function: cubic-bezier(0.53, 0.21, 0.29, 0.67) !important;
+2
View File
@@ -56,6 +56,8 @@ export type ExtensionVirtualWorkspaceSupport = {
export interface IProductConfiguration {
readonly version: string;
readonly voidVersion: string; // Void added this
readonly release: string; // VSCodium added this
readonly date?: string;
readonly quality?: string;
readonly commit?: string;
+3 -3
View File
@@ -125,7 +125,7 @@ import { IMetricsService } from '../../workbench/contrib/void/common/metricsServ
import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpdateService.js';
import { MetricsMainService } from '../../workbench/contrib/void/electron-main/metricsMainService.js';
import { VoidMainUpdateService } from '../../workbench/contrib/void/electron-main/voidUpdateMainService.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/llmMessageChannel.js';
import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js';
/**
* The main VS Code application. There will only ever be one instance,
@@ -1256,8 +1256,8 @@ export class CodeApplication extends Disposable {
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);
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
// Extension Host Debug Broadcasting
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
+1 -1
View File
@@ -3570,7 +3570,7 @@ class EditorQuickSuggestions extends BaseEditorOption<EditorOption.quickSuggesti
const defaults: InternalQuickSuggestionsOptions = {
other: 'on',
comments: 'off',
strings: 'off'
strings: 'on' // Void changed this setting
};
const types: IJSONSchema[] = [
{ type: 'boolean' },
@@ -230,7 +230,7 @@ export class DiagnosticsService implements IDiagnosticsService {
private formatEnvironment(info: IMainProcessDiagnostics): string {
const output: string[] = [];
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
output.push(`Version: ${this.productService.nameShort} ${this.productService.version} ${this.productService.release || 'Release unknown'} (${this.productService.commit || 'Commit unknown'}, ${this.productService.date || 'Date unknown'})`);
output.push(`OS Version: ${osLib.type()} ${osLib.arch()} ${osLib.release()}`);
const cpus = osLib.cpus();
if (cpus && cpus.length > 0) {
@@ -52,7 +52,6 @@ import { IProductService } from '../../product/common/productService.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUriIdentityService } from '../../uriIdentity/common/uriIdentity.js';
import { IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js';
import { IConfigurationService } from '../../configuration/common/configuration.js';
import { isLinux } from '../../../base/common/platform.js';
export const INativeServerExtensionManagementService = refineServiceDecorator<IExtensionManagementService, INativeServerExtensionManagementService>(IExtensionManagementService);
@@ -85,7 +84,6 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
@IDownloadService private downloadService: IDownloadService,
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IFileService private readonly fileService: IFileService,
@IConfigurationService private readonly configurationService: IConfigurationService,
@IProductService productService: IProductService,
@IAllowedExtensionsService allowedExtensionsService: IAllowedExtensionsService,
@IUriIdentityService uriIdentityService: IUriIdentityService,
@@ -324,8 +322,7 @@ export class ExtensionManagementService extends AbstractExtensionManagementServi
private async downloadExtension(extension: IGalleryExtension, operation: InstallOperation, verifySignature: boolean, clientTargetPlatform?: TargetPlatform): Promise<{ readonly location: URI; readonly verificationStatus: ExtensionSignatureVerificationCode | undefined }> {
if (verifySignature) {
const value = this.configurationService.getValue('extensions.verifySignature');
verifySignature = isBoolean(value) ? value : true;
verifySignature = false; // Void disable-signature-verification-patch
}
const { location, verificationStatus } = await this.extensionsDownloader.download(extension, operation, verifySignature, clientTargetPlatform);
@@ -404,7 +404,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
}
private async getShellCommandLink(): Promise<{ readonly source: string; readonly target: string }> {
const target = resolve(this.environmentMainService.appRoot, 'bin', 'code');
const target = resolve(this.environmentMainService.appRoot, 'bin', this.productService.applicationName);
const source = `/usr/local/bin/${this.productService.applicationName}`;
// Ensure source exists
@@ -639,7 +639,7 @@ export class NativeHostMainService extends Disposable implements INativeHostMain
// macOS
if (this.environmentMainService.isBuilt) {
return join(this.environmentMainService.appRoot, 'bin', 'code');
return join(this.environmentMainService.appRoot, 'bin', `${this.productService.applicationName}`);
}
return join(this.environmentMainService.appRoot, 'scripts', 'code-cli.sh');
+3 -2
View File
@@ -43,10 +43,11 @@ else if (globalThis._VSCODE_PRODUCT_JSON && globalThis._VSCODE_PACKAGE_JSON) {
// want to have it running out of sources so we
// read it from package.json only when we need it.
if (!product.version) {
const pkg = globalThis._VSCODE_PACKAGE_JSON as { version: string };
const pkg = globalThis._VSCODE_PACKAGE_JSON as { version: string, release: string };
Object.assign(product, {
version: pkg.version
version: pkg.version,
release: pkg.release
});
}
}
+40 -1
View File
@@ -49,7 +49,8 @@ export const enum StateType {
export const enum UpdateType {
Setup,
Archive,
Snap
Snap,
WindowsInstaller
}
export const enum DisablementReason {
@@ -108,3 +109,41 @@ export interface IUpdateService {
isLatestVersion(): Promise<boolean | undefined>;
_applySpecificUpdate(packagePath: string): Promise<void>;
}
export type Architecture =
| 'arm'
| 'arm64'
| 'ia32'
| 'loong64'
| 'mips'
| 'mipsel'
| 'ppc'
| 'ppc64'
| 'riscv64'
| 's390'
| 's390x'
| 'x64';
export type Platform =
| 'aix'
| 'android'
| 'darwin'
| 'freebsd'
| 'haiku'
| 'linux'
| 'openbsd'
| 'sunos'
| 'win32'
| 'cygwin'
| 'netbsd';
export type Quality =
| 'insider'
| 'stable';
export type Target =
| 'archive'
| 'msi'
| 'system'
| 'user';
@@ -4,6 +4,7 @@
*--------------------------------------------------------------------------------------------*/
// 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';
@@ -13,13 +14,16 @@ import { ILifecycleMainService, LifecycleMainPhase } from '../../lifecycle/elect
import { ILogService } from '../../log/common/log.js';
import { IProductService } from '../../product/common/productService.js';
import { IRequestService } from '../../request/common/request.js';
import { AvailableForDownload, DisablementReason, IUpdateService, State, StateType, UpdateType } from '../common/update.js';
import { Architecture, AvailableForDownload, DisablementReason, IUpdateService, Platform, State, StateType, Target, UpdateType } from '../common/update.js';
export function createUpdateURL(platform: string, quality: string, productService: IProductService): string {
// 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}`;
// Void - VSCodium's version-1-update.patch
export function createUpdateURL(productService: IProductService, quality: string, platform: Platform, architecture: Architecture, target?: Target): string { // return `https://voideditor.dev/api/update/${platform}/stable`;
if (target) {
return `${productService.updateUrl}/${quality}/${platform}/${architecture}/${target}/latest.json`;
} else { // we shouldn't usually have a target:
// https://raw.githubusercontent.com/voideditor/versions/refs/heads/main/stable/darwin/arm64/latest.json
return `${productService.updateUrl}/${quality}/${platform}/${architecture}/latest.json`;
}
}
export type UpdateErrorClassification = {
@@ -81,22 +85,20 @@ export abstract class AbstractUpdateService implements IUpdateService {
return;
}
this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
// Void - re-enabled auto updates
// this.setState(State.Disabled(DisablementReason.ManuallyDisabled));
// Void - temporarily disabled while we figure out how to do this the right way
// this.setState(State.Idle(this.getUpdateType()));
this.setState(State.Idle(this.getUpdateType()));
// start checking for updates after 10 seconds
// this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
this.scheduleCheckForUpdates(10 * 1000).then(undefined, err => this.logService.error(err));
}
// private async scheduleCheckForUpdates(delay = 60 * 60 * 1000): Promise<void> {
// await timeout(delay);
// await this.checkForUpdates(false);
// return await 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);
@@ -13,11 +13,12 @@ import { IEnvironmentMainService } from '../../environment/electron-main/environ
import { ILifecycleMainService, IRelaunchHandler, IRelaunchOptions } from '../../lifecycle/electron-main/lifecycleMainService.js';
import { ILogService } from '../../log/common/log.js';
import { IProductService } from '../../product/common/productService.js';
import { IRequestService } from '../../request/common/request.js';
import { IRequestService, asJson } from '../../request/common/request.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { IUpdate, State, StateType, UpdateType } from '../common/update.js';
import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import * as semver from 'semver';
export class DarwinUpdateService extends AbstractUpdateService implements IRelaunchHandler {
private readonly disposables = new DisposableStore();
@@ -75,13 +76,7 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
let assetID: string;
if (!this.productService.darwinUniversalAssetId) {
assetID = process.arch === 'x64' ? 'darwin' : 'darwin-arm64';
} else {
assetID = this.productService.darwinUniversalAssetId;
}
const url = createUpdateURL(assetID, quality, this.productService);
const url = createUpdateURL(this.productService, quality, process.platform, process.arch);
try {
electron.autoUpdater.setFeedURL({ url });
} catch (e) {
@@ -93,8 +88,36 @@ export class DarwinUpdateService extends AbstractUpdateService implements IRelau
}
protected doCheckForUpdates(context: any): void {
if (!this.url) {
return;
}
this.setState(State.CheckingForUpdates(context));
electron.autoUpdater.checkForUpdates();
// electron.autoUpdater.checkForUpdates();
this.requestService.request({ url: this.url }, CancellationToken.None)
.then<IUpdate | null>(asJson)
.then(update => {
if (!update || !update.url || !update.version || !update.productVersion) {
this.setState(State.Idle(UpdateType.Setup));
return Promise.resolve(null);
}
const fetchedVersion = update.productVersion.replace(/(\d+\.\d+\.\d+)(?:\.(\d+))(\-\w+)?/, '$1$3+$2');
const currentVersion = `${this.productService.voidVersion}+${this.productService.release}`;
// Void compares voidVersion, not VSCode version
// const currentVersion = `${this.productService.version}+${this.productService.release}`;
if (semver.compareBuild(currentVersion, fetchedVersion) >= 0) {
this.setState(State.Idle(UpdateType.Setup));
}
else {
electron.autoUpdater.checkForUpdates();
}
return Promise.resolve(null);
})
}
private onUpdateAvailable(): void {
@@ -30,7 +30,7 @@ export class LinuxUpdateService extends AbstractUpdateService {
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
protected doBuildUpdateFeedUrl(quality: string): string {
return createUpdateURL(`linux-${process.arch}`, quality, this.productService);
return createUpdateURL(this.productService, quality, process.platform, process.arch);
}
protected doCheckForUpdates(context: any): void {
@@ -9,7 +9,6 @@ import { tmpdir } from 'os';
import { timeout } from '../../../base/common/async.js';
import { CancellationToken } from '../../../base/common/cancellation.js';
import { memoize } from '../../../base/common/decorators.js';
import { hash } from '../../../base/common/hash.js';
import * as path from '../../../base/common/path.js';
import { URI } from '../../../base/common/uri.js';
import { checksum } from '../../../base/node/crypto.js';
@@ -23,8 +22,9 @@ import { INativeHostMainService } from '../../native/electron-main/nativeHostMai
import { IProductService } from '../../product/common/productService.js';
import { asJson, IRequestService } from '../../request/common/request.js';
import { ITelemetryService } from '../../telemetry/common/telemetry.js';
import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, UpdateType } from '../common/update.js';
import { AbstractUpdateService, createUpdateURL, UpdateErrorClassification } from './abstractUpdateService.js';
import { AvailableForDownload, DisablementReason, IUpdate, State, StateType, Target, UpdateType } from '../common/update.js';
import { AbstractUpdateService, createUpdateURL } from './abstractUpdateService.js';
import * as semver from 'semver';
async function pollUntil(fn: () => boolean, millis = 1000): Promise<void> {
while (!fn()) {
@@ -40,9 +40,13 @@ interface IAvailableUpdate {
let _updateType: UpdateType | undefined = undefined;
function getUpdateType(): UpdateType {
if (typeof _updateType === 'undefined') {
_updateType = fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))
? UpdateType.Setup
: UpdateType.Archive;
if (fs.existsSync(path.join(path.dirname(process.execPath), 'unins000.exe'))) {
_updateType = UpdateType.Setup;
} else if (path.basename(path.normalize(path.join(process.execPath, '..', '..'))) === 'Program Files') {
_updateType = UpdateType.WindowsInstaller;
} else {
_updateType = UpdateType.Archive;
}
}
return _updateType;
@@ -61,6 +65,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
constructor(
@ILifecycleMainService lifecycleMainService: ILifecycleMainService,
@IConfigurationService configurationService: IConfigurationService,
// @ts-expect-error
@ITelemetryService private readonly telemetryService: ITelemetryService,
@IEnvironmentMainService environmentMainService: IEnvironmentMainService,
@IRequestService requestService: IRequestService,
@@ -101,15 +106,24 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
// Void: buildUpdateFeedUrl -> doBuildUpdateFeedUrl
protected doBuildUpdateFeedUrl(quality: string): string | undefined {
let platform = `win32-${process.arch}`;
if (getUpdateType() === UpdateType.Archive) {
platform += '-archive';
} else if (this.productService.target === 'user') {
platform += '-user';
let target: Target;
switch (getUpdateType()) {
case UpdateType.Archive:
target = 'archive'
break;
case UpdateType.WindowsInstaller:
target = 'msi'
break;
default:
if (this.productService.target === 'user') {
target = 'user'
}
else {
target = 'system'
}
}
return createUpdateURL(platform, quality, this.productService);
return createUpdateURL(this.productService, quality, process.platform, process.arch, target);
}
protected doCheckForUpdates(context: any): void {
@@ -129,6 +143,16 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
return Promise.resolve(null);
}
const fetchedVersion = update.productVersion.replace(/(\d+\.\d+\.\d+)(?:\.(\d+))(\-\w+)?/, '$1$3+$2');
const currentVersion = `${this.productService.voidVersion}+${this.productService.release}`;
// Void compares voidVersion, not VSCode version
// const currentVersion = `${this.productService.version}+${this.productService.release}`;
if (semver.compareBuild(currentVersion, fetchedVersion) >= 0) {
this.setState(State.Idle(updateType));
return Promise.resolve(null);
}
if (updateType === UpdateType.Archive) {
this.setState(State.AvailableForDownload(update));
return Promise.resolve(null);
@@ -155,7 +179,7 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
this.availableUpdate = { packagePath };
this.setState(State.Downloaded(update));
const fastUpdatesEnabled = this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
const fastUpdatesEnabled = getUpdateType() === UpdateType.Setup && this.configurationService.getValue('update.enableWindowsBackgroundUpdates');
if (fastUpdatesEnabled) {
if (this.productService.target === 'user') {
this.doApplyUpdate();
@@ -167,7 +191,6 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
});
})
.then(undefined, err => {
this.telemetryService.publicLog2<{ messageHash: string }, UpdateErrorClassification>('update:error', { messageHash: String(hash(String(err))) });
this.logService.error(err);
// only show message when explicitly checking for updates
@@ -251,10 +274,18 @@ export class Win32UpdateService extends AbstractUpdateService implements IRelaun
if (this.availableUpdate.updateFilePath) {
fs.unlinkSync(this.availableUpdate.updateFilePath);
} else {
spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore']
});
const type = getUpdateType();
if (type === UpdateType.WindowsInstaller) {
spawn('msiexec.exe', ['/i', this.availableUpdate.packagePath], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore']
});
} else {
spawn(this.availableUpdate.packagePath, ['/silent', '/log', '/mergetasks=runcode,!desktopicon,!quicklaunchicon'], {
detached: true,
stdio: ['ignore', 'ignore', 'ignore']
});
}
}
}
@@ -21,6 +21,7 @@ import { MarkdownRenderer, openLinkFromMarkdown } from '../../../../editor/brows
import { defaultButtonStyles, defaultCheckboxStyles, defaultDialogStyles, defaultInputBoxStyles } from '../../../../platform/theme/browser/defaultStyles.js';
import { ResultKind } from '../../../../platform/keybinding/common/keybindingResolver.js';
import { IOpenerService } from '../../../../platform/opener/common/opener.js';
import { getReleaseString } from '../../../../workbench/common/release.js';
export class BrowserDialogHandler extends AbstractDialogHandler {
@@ -79,13 +80,14 @@ export class BrowserDialogHandler extends AbstractDialogHandler {
async about(): Promise<void> {
const detailString = (useAgo: boolean): string => {
const releaseString = getReleaseString();
return localize('aboutDetail',
"Version: {0}\nCommit: {1}\nDate: {2}\nBrowser: {3}",
this.productService.version || 'Unknown',
this.productService.commit || 'Unknown',
this.productService.date ? `${this.productService.date}${useAgo ? ' (' + fromNow(new Date(this.productService.date), true) + ')' : ''}` : 'Unknown',
navigator.userAgent
);
).replace('\n', `\n${releaseString} ${this.productService.release || 'Unknown'}\n`);
};
const detail = detailString(true);
@@ -250,7 +250,9 @@ export class EditorGroupWatermark extends Disposable {
linkSpan.appendChild(dirSpan);
return linkSpan
}).filter(v => !!v)
})
.filter(v => !!v)
.slice(0, 5) // take 5 most recent
)
}
+15
View File
@@ -0,0 +1,15 @@
// added by VSCodium
import { language } from '../../../vs/base/common/platform.js';
const DEFAULT_LABEL = 'Release:';
const LABELS: { [key: string]: string } = {
'en': DEFAULT_LABEL,
'fr': 'Révision :',
'ru': 'Релиз:',
'zh-hans': '发布版本:',
'zh-hant': '發布版本:',
};
export function getReleaseString(): string {
return LABELS[language] ?? DEFAULT_LABEL;
}
@@ -4,23 +4,20 @@
*--------------------------------------------------------------------------------------------*/
import { coalesce, isNonEmptyArray } from '../../../../base/common/arrays.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { toErrorMessage } from '../../../../base/common/errorMessage.js';
import { Event } from '../../../../base/common/event.js';
import { MarkdownString } from '../../../../base/common/htmlContent.js';
import { KeyCode, KeyMod } from '../../../../base/common/keyCodes.js';
import { Disposable, DisposableMap, DisposableStore } from '../../../../base/common/lifecycle.js';
import * as strings from '../../../../base/common/strings.js';
import { localize, localize2 } from '../../../../nls.js';
import { ContextKeyExpr, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { localize } from '../../../../nls.js';
import { IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { ExtensionIdentifier, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js';
import { SyncDescriptor } from '../../../../platform/instantiation/common/descriptors.js';
import { ILogService } from '../../../../platform/log/common/log.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { Registry } from '../../../../platform/registry/common/platform.js';
import { ViewPaneContainer } from '../../../browser/parts/views/viewPaneContainer.js';
import { IWorkbenchContribution } from '../../../common/contributions.js';
import { IViewContainersRegistry, IViewDescriptor, IViewsRegistry, ViewContainer, ViewContainerLocation, Extensions as ViewExtensions } from '../../../common/views.js';
import { IViewsRegistry, Extensions as ViewExtensions } from '../../../common/views.js';
import { IExtensionFeatureTableRenderer, IRenderedData, ITableData, IRowData, IExtensionFeaturesRegistry, Extensions } from '../../../services/extensionManagement/common/extensionFeatures.js';
import { isProposedApiEnabled } from '../../../services/extensions/common/extensions.js';
import * as extensionsRegistry from '../../../services/extensions/common/extensionsRegistry.js';
@@ -30,90 +27,91 @@ import { ChatAgentLocation, IChatAgentData, IChatAgentService } from '../common/
import { ChatContextKeys } from '../common/chatContextKeys.js';
import { IRawChatParticipantContribution } from '../common/chatParticipantContribTypes.js';
import { ChatViewId } from './chat.js';
import { CHAT_EDITING_SIDEBAR_PANEL_ID, CHAT_SIDEBAR_PANEL_ID, ChatViewPane } from './chatViewPane.js';
// --- Chat Container & View Registration
const chatViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
id: CHAT_SIDEBAR_PANEL_ID,
title: localize2('chat.viewContainer.label', "Chat"),
icon: Codicon.commentDiscussion,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
storageId: CHAT_SIDEBAR_PANEL_ID,
hideIfEmpty: true,
order: 100,
}, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true });
// Void commented this out
// const chatViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
// id: CHAT_SIDEBAR_PANEL_ID,
// title: localize2('chat.viewContainer.label', "Chat"),
// icon: Codicon.commentDiscussion,
// ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
// storageId: CHAT_SIDEBAR_PANEL_ID,
// hideIfEmpty: true,
// order: 100,
// }, ViewContainerLocation.AuxiliaryBar, { isDefault: true, doNotRegisterOpenCommand: true });
const chatViewDescriptor: IViewDescriptor[] = [{
id: ChatViewId,
containerIcon: chatViewContainer.icon,
containerTitle: chatViewContainer.title.value,
singleViewPaneContainerTitle: chatViewContainer.title.value,
name: localize2('chat.viewContainer.label', "Chat"),
canToggleVisibility: false,
canMoveView: true,
openCommandActionDescriptor: {
id: CHAT_SIDEBAR_PANEL_ID,
title: chatViewContainer.title,
mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"),
keybindings: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
mac: {
primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
}
},
order: 1
},
ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]),
when: ContextKeyExpr.or(
ChatContextKeys.Setup.hidden.negate(),
ChatContextKeys.Setup.installed,
ChatContextKeys.panelParticipantRegistered,
ChatContextKeys.extensionInvalid
)
}];
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(chatViewDescriptor, chatViewContainer);
// const chatViewDescriptor: IViewDescriptor[] = [{
// id: ChatViewId,
// containerIcon: chatViewContainer.icon,
// containerTitle: chatViewContainer.title.value,
// singleViewPaneContainerTitle: chatViewContainer.title.value,
// name: localize2('chat.viewContainer.label', "Chat"),
// canToggleVisibility: false,
// canMoveView: true,
// openCommandActionDescriptor: {
// id: CHAT_SIDEBAR_PANEL_ID,
// title: chatViewContainer.title,
// mnemonicTitle: localize({ key: 'miToggleChat', comment: ['&& denotes a mnemonic'] }, "&&Chat"),
// keybindings: {
// primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyCode.KeyI,
// mac: {
// primary: KeyMod.CtrlCmd | KeyMod.WinCtrl | KeyCode.KeyI
// }
// },
// order: 1
// },
// ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.Panel }]),
// when: ContextKeyExpr.or(
// ChatContextKeys.Setup.hidden.negate(),
// ChatContextKeys.Setup.installed,
// ChatContextKeys.panelParticipantRegistered,
// ChatContextKeys.extensionInvalid
// )
// }];
// Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(chatViewDescriptor, chatViewContainer);
// --- Edits Container & View Registration
const editsViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
id: CHAT_EDITING_SIDEBAR_PANEL_ID,
title: localize2('chatEditing.viewContainer.label', "Copilot Edits"),
icon: Codicon.editSession,
ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_EDITING_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
storageId: CHAT_EDITING_SIDEBAR_PANEL_ID,
hideIfEmpty: true,
order: 101,
}, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true });
// Void commented this out
// const editsViewContainer: ViewContainer = Registry.as<IViewContainersRegistry>(ViewExtensions.ViewContainersRegistry).registerViewContainer({
// id: CHAT_EDITING_SIDEBAR_PANEL_ID,
// title: localize2('chatEditing.viewContainer.label', "Copilot Edits"),
// icon: Codicon.editSession,
// ctorDescriptor: new SyncDescriptor(ViewPaneContainer, [CHAT_EDITING_SIDEBAR_PANEL_ID, { mergeViewWithContainerWhenSingleView: true }]),
// storageId: CHAT_EDITING_SIDEBAR_PANEL_ID,
// hideIfEmpty: true,
// order: 101,
// }, ViewContainerLocation.AuxiliaryBar, { doNotRegisterOpenCommand: true });
const editsViewDescriptor: IViewDescriptor[] = [{
id: 'workbench.panel.chat.view.edits',
containerIcon: editsViewContainer.icon,
containerTitle: editsViewContainer.title.value,
singleViewPaneContainerTitle: editsViewContainer.title.value,
name: editsViewContainer.title,
canToggleVisibility: false,
canMoveView: true,
openCommandActionDescriptor: {
id: CHAT_EDITING_SIDEBAR_PANEL_ID,
title: editsViewContainer.title,
mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"),
keybindings: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
linux: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI
}
},
order: 2
},
ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]),
when: ContextKeyExpr.or(
ChatContextKeys.Setup.hidden.negate(),
ChatContextKeys.Setup.installed,
ChatContextKeys.editingParticipantRegistered
)
}];
Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer);
// const editsViewDescriptor: IViewDescriptor[] = [{
// id: 'workbench.panel.chat.view.edits',
// containerIcon: editsViewContainer.icon,
// containerTitle: editsViewContainer.title.value,
// singleViewPaneContainerTitle: editsViewContainer.title.value,
// name: editsViewContainer.title,
// canToggleVisibility: false,
// canMoveView: true,
// openCommandActionDescriptor: {
// id: CHAT_EDITING_SIDEBAR_PANEL_ID,
// title: editsViewContainer.title,
// mnemonicTitle: localize({ key: 'miToggleEdits', comment: ['&& denotes a mnemonic'] }, "Copilot Ed&&its"),
// keybindings: {
// primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyI,
// linux: {
// primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.KeyI
// }
// },
// order: 2
// },
// ctorDescriptor: new SyncDescriptor(ChatViewPane, [{ location: ChatAgentLocation.EditingSession }]),
// when: ContextKeyExpr.or(
// ChatContextKeys.Setup.hidden.negate(),
// ChatContextKeys.Setup.installed,
// ChatContextKeys.editingParticipantRegistered
// )
// }];
// Registry.as<IViewsRegistry>(ViewExtensions.ViewsRegistry).registerViews(editsViewDescriptor, editsViewContainer);
const chatParticipantExtensionPoint = extensionsRegistry.ExtensionsRegistry.registerExtensionPoint<IRawChatParticipantContribution[]>({
extensionPoint: 'chatParticipants',
@@ -5,7 +5,7 @@
import { Disposable, DisposableStore } from '../../../../base/common/lifecycle.js';
import { localize } from '../../../../nls.js';
import { Action2, MenuId, MenuRegistry, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js';
import { IEnvironmentService } from '../../../../platform/environment/common/environment.js';
import { IFileService } from '../../../../platform/files/common/files.js';
@@ -15,7 +15,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { createSyncHeaders, IAuthenticationProvider, IResourceRefHandle } from '../../../../platform/userDataSync/common/userDataSync.js';
import { AuthenticationSession, AuthenticationSessionsChangeEvent, IAuthenticationService } from '../../../services/authentication/common/authentication.js';
import { IExtensionService } from '../../../services/extensions/common/extensions.js';
import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService, SyncResource, EDIT_SESSIONS_PENDING_KEY } from '../common/editSessions.js';
import { EDIT_SESSIONS_SIGNED_IN, EditSession, EDIT_SESSION_SYNC_CATEGORY, IEditSessionsStorageService, EDIT_SESSIONS_SIGNED_IN_KEY, IEditSessionsLogService, SyncResource } from '../common/editSessions.js';
import { IDialogService } from '../../../../platform/dialogs/common/dialogs.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { getCurrentAuthenticationSessionInfo } from '../../../services/authentication/browser/authenticationService.js';
@@ -91,7 +91,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
// If another window changes the preferred session storage, reset our cached auth state in memory
this._register(this.storageService.onDidChangeValue(StorageScope.APPLICATION, EditSessionsWorkbenchService.CACHED_SESSION_STORAGE_KEY, this._store)(() => this.onDidChangeStorage()));
this.registerSignInAction();
this.registerResetAuthenticationAction();
this.signedInContext = EDIT_SESSIONS_SIGNED_IN.bindTo(this.contextKeyService);
@@ -454,46 +453,6 @@ export class EditSessionsWorkbenchService extends Disposable implements IEditSes
}
}
private registerSignInAction() {
if (!this.serverConfiguration?.url) {
return;
}
const that = this;
const id = 'workbench.editSessions.actions.signIn';
const when = ContextKeyExpr.and(ContextKeyExpr.equals(EDIT_SESSIONS_PENDING_KEY, false), ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false));
this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 {
constructor() {
super({
id,
title: localize('sign in', 'Turn on Cloud Changes...'),
category: EDIT_SESSION_SYNC_CATEGORY,
precondition: when,
menu: [{
id: MenuId.CommandPalette,
},
{
id: MenuId.AccountsContext,
group: '2_editSessions',
when,
}]
});
}
async run() {
return await that.initialize('write', false);
}
}));
this._register(MenuRegistry.appendMenuItem(MenuId.AccountsContext, {
group: '2_editSessions',
command: {
id,
title: localize('sign in badge', 'Turn on Cloud Changes... (1)'),
},
when: ContextKeyExpr.and(ContextKeyExpr.equals(EDIT_SESSIONS_PENDING_KEY, true), ContextKeyExpr.equals(EDIT_SESSIONS_SIGNED_IN_KEY, false))
}));
}
private registerResetAuthenticationAction() {
const that = this;
this._register(registerAction2(class ResetEditSessionAuthenticationAction extends Action2 {
@@ -27,6 +27,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js';
import { IExplorerService } from './files.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Categories } from '../../../../platform/action/common/actionCommonCategories.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../void/browser/voidSettingsPane.js';
// Contribute Global Actions
@@ -675,6 +676,18 @@ for (const menuId of [MenuId.EmptyEditorGroupContext, MenuId.EditorTabsBarContex
// File menu
// Void added this:
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
group: '0_void',
command: {
id: VOID_OPEN_SETTINGS_ACTION_ID,
title: nls.localize({ key: 'openVoid', comment: ['&& denotes a mnemonic'] }, "&&Open Void Settings"),
},
order: 1
});
MenuRegistry.appendMenuItem(MenuId.MenubarFileMenu, {
group: '1_new',
command: {
@@ -39,7 +39,7 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService {
console.log(`----------------------------------------------`);
console.log(`${error.resource.toString()}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file
console.log(`${error.resource.fsPath}: ${error.startLineNumber} ${error.message} ${error.severity}`); // ! all errors in the file
try {
// Get the text model for the file
@@ -122,11 +122,11 @@ class MarkerCheckService extends Disposable implements IMarkerCheckService {
// const markers = this._markerService.read({ resource });
// if (markers.length === 0) {
// console.log(`${resource.toString()}: No diagnostics`);
// console.log(`${resource.fsPath}: No diagnostics`);
// continue;
// }
// console.log(`Diagnostics for ${resource.toString()}:`);
// console.log(`Diagnostics for ${resource.fsPath}:`);
// markers.forEach(marker => this._logMarker(marker));
// }
// };
@@ -34,7 +34,7 @@
// // const result = await new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // tools: ['grep_search'],
// // onFinalMessage: ({ result: r, }) => {
// // res(r)
// // },
@@ -73,7 +73,7 @@
// // const result = new Promise((res, rej) => {
// // sendLLMMessage({
// // messages,
// // tools: ['search'],
// // tools: ['grep_search'],
// // onResult: (r) => {
// // res(r)
// // }
@@ -15,12 +15,19 @@ import { IEditorService } from '../../../services/editor/common/editorService.js
import { isCodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { EditorResourceAccessor } from '../../../common/editor.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js';
import { extractCodeFromRegular } from '../common/helpers/extractCodeFromResult.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ILLMMessageService } from '../common/llmMessageService.js';
import { _ln, allLinebreakSymbols } from '../common/voidFileService.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { isWindows } from '../../../../base/common/platform.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { FeatureName } from '../common/voidSettingsTypes.js';
// import { IContextGatheringService } from './contextGatheringService.js';
const allLinebreakSymbols = ['\r\n', '\n']
const _ln = isWindows ? allLinebreakSymbols[0] : allLinebreakSymbols[1]
// The extension this was called from is here - https://github.com/voideditor/void/blob/autocomplete/extensions/void/src/extension/extension.ts
@@ -630,9 +637,12 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
token: CancellationToken,
): Promise<InlineCompletion[]> {
const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete
if (!isEnabled) return []
const testMode = false
const docUriStr = model.uri.toString();
const docUriStr = model.uri.fsPath;
const prefixAndSuffix = getPrefixAndSuffixInfo(model, position)
const { prefix, suffix } = prefixAndSuffix
@@ -762,8 +772,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
// console.log('B')
// create a new autocompletion and add it to cache
const newAutocompletion: Autocompletion = {
id: this._autocompletionId++,
@@ -783,6 +791,11 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
console.log('starting autocomplete...', predictionType)
const featureName: FeatureName = 'Autocomplete'
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined
// set parameters of `newAutocompletion` appropriately
newAutocompletion.llmPromise = new Promise((resolve, reject) => {
@@ -793,7 +806,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
suffix: llmSuffix,
stopTokens: stopTokens,
},
useProviderFor: 'Autocomplete',
modelSelection,
modelSelectionOptions,
logging: { loggingName: 'Autocomplete' },
onText: () => { }, // unused in FIMMessage
// onText: async ({ fullText, newText }) => {
@@ -838,6 +852,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
newAutocompletion.status = 'error'
reject(message)
},
onAbort: () => { },
})
newAutocompletion.requestId = requestId
@@ -877,6 +892,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IEditorService private readonly _editorService: IEditorService,
@IModelService private readonly _modelService: IModelService,
@IVoidSettingsService private readonly _settingsService: IVoidSettingsService,
// @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService,
) {
super()
@@ -900,7 +916,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
if (!resource) return;
const model = this._modelService.getModel(resource)
if (!model) return;
const docUriStr = resource.toString();
const docUriStr = resource.fsPath;
if (!this._autocompletionsOfDocument[docUriStr]) return;
const { prefix, } = getPrefixAndSuffixInfo(model, position)
@@ -929,4 +945,3 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
registerWorkbenchContribution2(AutocompleteService.ID, AutocompleteService, WorkbenchPhase.BlockRestore);
File diff suppressed because it is too large Load Diff
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,64 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Event } from '../../../../base/common/event.js';
import { URI } from '../../../../base/common/uri.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { Diff, DiffArea } from './editCodeService.js';
export type StartBehavior = 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts'
export type StartApplyingOpts = ({
from: 'QuickEdit';
diffareaid: number; // id of the CtrlK area (contains text selection)
startBehavior: StartBehavior;
} | {
from: 'ClickApply';
applyStr: string;
uri: 'current' | URI;
startBehavior: StartBehavior;
})
export type AddCtrlKOpts = {
startLine: number,
endLine: number,
editor: ICodeEditor,
}
export const IEditCodeService = createDecorator<IEditCodeService>('editCodeService');
export interface IEditCodeService {
readonly _serviceBrand: undefined;
startApplying(opts: StartApplyingOpts): Promise<[URI, Promise<void>] | null>;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
diffAreaOfId: Record<string, DiffArea>;
diffAreasOfURI: Record<string, Set<string> | undefined>;
diffOfId: Record<string, Diff>;
acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void;
// events
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
onDidChangeDiffsInDiffZone: Event<{ uri: URI; diffareaid: number }>; // only fires when not streaming!!! streaming would be too much
onDidChangeStreamingInDiffZone: Event<{ uri: URI; diffareaid: number }>;
onDidChangeStreamingInCtrlKZone: Event<{ uri: URI; diffareaid: number }>;
// CtrlKZone streaming state
isCtrlKZoneStreaming(opts: { diffareaid: number }): boolean;
interruptCtrlKStreaming(opts: { diffareaid: number }): void;
// // DiffZone codeBoxId streaming state
interruptURIStreaming(opts: { uri: URI }): void;
// testDiffs(): void;
}
@@ -25,7 +25,7 @@ export interface IConsistentItemService {
export const IConsistentItemService = createDecorator<IConsistentItemService>('ConsistentItemService');
export class ConsistentItemService extends Disposable {
export class ConsistentItemService extends Disposable implements IConsistentItemService {
readonly _serviceBrand: undefined
@@ -1,174 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// eg "bash" -> "shell"
export const nameToVscodeLanguage: { [key: string]: string } = {
// Web Technologies
'html': 'html',
'css': 'css',
'scss': 'scss',
'sass': 'scss',
'less': 'less',
'javascript': 'typescript',
'js': 'typescript', // use more general renderer
'jsx': 'typescript',
'typescript': 'typescript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'python': 'python',
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'c++': 'cpp',
'c': 'c',
'csharp': 'csharp',
'cs': 'csharp',
'c#': 'csharp',
'go': 'go',
'golang': 'go',
'rust': 'rust',
'rs': 'rust',
'ruby': 'ruby',
'rb': 'ruby',
'php': 'php',
'shell': 'shell',
'bash': 'shell',
'sh': 'shell',
'zsh': 'shell',
// Markup and Config
'markdown': 'markdown',
'md': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Database and Query Languages
'sql': 'sql',
'mysql': 'sql',
'postgresql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
// Others
'dockerfile': 'dockerfile',
'docker': 'dockerfile',
'makefile': 'makefile',
'plaintext': 'plaintext',
'text': 'plaintext'
};
// eg ".ts" -> "typescript"
const fileExtensionToVscodeLanguage: { [key: string]: string } = {
// Web
'html': 'html',
'htm': 'html',
'css': 'css',
'scss': 'scss',
'less': 'less',
'js': 'javascript',
'jsx': 'javascript',
'ts': 'typescript',
'tsx': 'typescript',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'cc': 'cpp',
'c': 'c',
'h': 'cpp',
'hpp': 'cpp',
'cs': 'csharp',
'go': 'go',
'rs': 'rust',
'rb': 'ruby',
'php': 'php',
'sh': 'shell',
'bash': 'shell',
'zsh': 'shell',
// Markup/Config
'md': 'markdown',
'markdown': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Other
'sql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
'dockerfile': 'dockerfile',
'docker': 'dockerfile',
'mk': 'makefile',
// Config Files and Dot Files
'npmrc': 'ini',
'env': 'ini',
'gitignore': 'ignore',
'dockerignore': 'ignore',
'eslintrc': 'json',
'babelrc': 'json',
'prettierrc': 'json',
'stylelintrc': 'json',
'editorconfig': 'ini',
'htaccess': 'apacheconf',
'conf': 'ini',
'config': 'ini',
// Package Files
'package': 'json',
'package-lock': 'json',
'gemfile': 'ruby',
'podfile': 'ruby',
'rakefile': 'ruby',
// Build Systems
'cmake': 'cmake',
'makefile': 'makefile',
'gradle': 'groovy',
// Shell Scripts
'bashrc': 'shell',
'zshrc': 'shell',
'fish': 'shell',
// Version Control
'gitconfig': 'ini',
'hgrc': 'ini',
'svnconfig': 'ini',
// Web Server
'nginx': 'nginx',
// Misc Config
'properties': 'properties',
'cfg': 'ini',
'reg': 'ini'
};
export function filenameToVscodeLanguage(filename: string): string | undefined {
const ext = filename.toLowerCase().split('.').pop();
if (!ext) return undefined;
return fileExtensionToVscodeLanguage[ext];
}
@@ -1,52 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js'
import { EndOfLinePreference } from '../../../../../editor/common/model.js'
import { IModelService } from '../../../../../editor/common/services/model.js'
import { IFileService } from '../../../../../platform/files/common/files.js'
// attempts to read URI of currently opened model, then of raw file
export const VSReadFile = async (uri: URI, modelService: IModelService, fileService: IFileService) => {
const modelResult = await _VSReadModel(modelService, uri)
if (modelResult) return modelResult
const fileResult = await _VSReadFileRaw(fileService, uri)
if (fileResult) return fileResult
return ''
}
// read files from VSCode. preferred (but appears to only work if the model of this URI already exists. If it doesn't use the other function.)
const _VSReadModel = async (modelService: IModelService, uri: URI): Promise<string | null> => {
// attempt to read saved model (doesn't work if application was reloaded...)
const model = modelService.getModel(uri)
if (model) {
return model.getValue(EndOfLinePreference.LF)
}
// backup logic - look at all opened models and check if they have the same `fsPath`
const models = modelService.getModels()
for (const model of models) {
if (model.uri.fsPath === uri.fsPath)
return model.getValue(EndOfLinePreference.LF);
}
return null
}
const _VSReadFileRaw = async (fileService: IFileService, uri: URI) => {
try {
const res = await fileService.readFile(uri)
const str = res.value.toString()
return str
} catch (e) {
return null
}
}
@@ -83,7 +83,7 @@
.void-scrollable-element::-webkit-scrollbar,
.void-scrollable-element *::-webkit-scrollbar {
width: 14px !important;
height: 14px !important;
height: 4px !important;
}
.void-scrollable-element::-webkit-scrollbar-track,
@@ -0,0 +1,56 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import * as dom from '../../../../base/browser/dom.js';
import { IMetricsService } from '../common/metricsService.js';
export interface IMetricsPollService {
readonly _serviceBrand: undefined;
}
const PING_EVERY_MS = 15 * 1000 * 60 // 15 minutes
export const IMetricsPollService = createDecorator<IMetricsPollService>('voidMetricsPollService');
class MetricsPollService extends Disposable implements IMetricsPollService {
_serviceBrand: undefined;
static readonly ID = 'voidMetricsPollService';
private readonly intervalID: number
constructor(
@IMetricsService private readonly metricsService: IMetricsService,
) {
super()
// initial state
const { window } = dom.getActiveWindow()
let i = 1
this.intervalID = window.setInterval(() => {
this.metricsService.capture('Alive', { iv1: i })
i += 1
}, PING_EVERY_MS)
}
override dispose() {
super.dispose()
const { window } = dom.getActiveWindow()
window.clearInterval(this.intervalID)
}
}
registerWorkbenchContribution2(MetricsPollService.ID, MetricsPollService, WorkbenchPhase.BlockRestore);
@@ -1,525 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js';
import { filenameToVscodeLanguage } from '../helpers/detectLanguage.js';
import { CodeSelection, StagingSelectionItem, FileSelection } from '../../common/chatThreadService.js';
import { IModelService } from '../../../../../editor/common/services/model.js';
import { os } from '../helpers/systemInfo.js';
import { IVoidFileService } from '../../common/voidFileService.js';
// this is just for ease of readability
export const tripleTick = ['```', '```']
export const editToolDesc_toolDescription = `\
A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \
Typically the best description you can give here is a high level view of the final code you'd like to see. For example, code excerpt(s) with "// ... existing code ..." comments to help you write less. \
However, you are allowed to describe the change using whatever text/language you like, especially if the change is better described without code. \
Do NOT output the whole file if possible, and try to write as LITTLE as needed to describe the change.`
export const chat_systemMessage = (workspaces: string[]) => `\
You are a coding assistant. You are given a list of instructions to follow \`INSTRUCTIONS\`, and optionally a list of relevant files \`FILES\`, and selections inside of files \`SELECTIONS\`.
Please respond to the user's query. The user's query is never invalid.
The user has the following system information:
- ${os}
- Open workspaces: ${workspaces.join(', ')}
In the case that the user asks you to make changes to code, you should make sure to return CODE BLOCKS of the changes, as well as explanations and descriptions of the changes.
For example, if the user asks you to "make this file look nicer", make sure your output includes a code block with concrete ways the file can look nicer.
- Do not re-write the entire file in the code block.
- You can write comments like "// ... existing code" to indicate existing code.
- Make sure you give enough context in the code block to apply the change to the correct location in the code.
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
If you are given tools:
- Only use tools if the user asks you to do something. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools.
- You are allowed to use tools without asking for permission.
- Feel free to use tools to gather context, make suggestions, etc.
- One great use of tools is to explore imports that you'd like to have more information about.
- Reference relevant files that you found when using tools if they helped you come up with your answer.
- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not even refer to "pages" of results, just say you're getting more results.
Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them.
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.
## EXAMPLE 1
FILES
math.ts
${tripleTick[0]}typescript
const addNumbers = (a, b) => a + b
const multiplyNumbers = (a, b) => a * b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
const vectorize = (...numbers) => {
return numbers // vector
}
const dot = (vector1: number[], vector2: number[]) => {
if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`)
let sum = 0
for (let i = 0; i < vector1.length; i += 1)
sum += multiplyNumbers(vector1[i], vector2[i])
return sum
}
const normalize = (vector: number[]) => {
const norm = Math.sqrt(dot(vector, vector))
for (let i = 0; i < vector.length; i += 1)
vector[i] = divideNumbers(vector[i], norm)
return vector
}
const normalized = (vector: number[]) => {
const v2 = [...vector] // clone vector
return normalize(v2)
}
${tripleTick[1]}
SELECTIONS
math.ts (lines 3:3)
${tripleTick[0]}typescript
const subtractNumbers = (a, b) => a - b
${tripleTick[1]}
INSTRUCTIONS
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
## ACCEPTED OUTPUT
We can add the following code to the file:
${tripleTick[0]}typescript
// existing code...
const subtractNumbers = (a, b) => a - b
const exponentiateNumbers = (a, b) => Math.pow(a, b)
const divideNumbers = (a, b) => a / b
// existing code...
const raiseAll = (vector: number[], power: number) => {
for (let i = 0; i < vector.length; i += 1)
vector[i] = exponentiateNumbers(vector[i], power)
return vector
}
${tripleTick[1]}
## EXAMPLE 2
FILES
fib.ts
${tripleTick[0]}typescript
const dfs = (root) => {
if (!root) return;
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
${tripleTick[1]}
SELECTIONS
fib.ts (lines 10:10)
${tripleTick[0]}typescript
return fib(n - 1) + fib(n - 2)
${tripleTick[1]}
INSTRUCTIONS
memoize results
## ACCEPTED OUTPUT
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
${tripleTick[0]}typescript
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
if (memo[n]) return memo[n]; // Check if result is already computed
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
${tripleTick[1]}
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
Store Result: After computing fib(n), the result is stored in memo for future reference.
## END EXAMPLES\
`
type FileSelnLocal = { fileURI: URI, content: string }
const stringifyFileSelection = ({ fileURI, content }: FileSelnLocal) => {
return `\
${fileURI.fsPath}
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${content}
${tripleTick[1]}
`
}
const stringifyCodeSelection = ({ fileURI, selectionStr, range }: CodeSelection) => {
return `\
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
${tripleTick[0]}${filenameToVscodeLanguage(fileURI.fsPath) ?? ''}
${selectionStr}
${tripleTick[1]}
`
}
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
const stringifyFileSelections = async (fileSelections: FileSelection[], voidFileService: IVoidFileService) => {
if (fileSelections.length === 0) return null
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
const content = await voidFileService.readFile(sel.fileURI) ?? failToReadStr
return { ...sel, content }
}))
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
}
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
return codeSelections.map(sel => stringifyCodeSelection(sel)).join('\n') || null
}
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
if (!currSelns) return ''
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
}
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => {
const selnsStr = stringifySelectionNames(currSelns)
let str = ''
if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` }
str += `\nINSTRUCTIONS\n${instructions}`
return str;
};
export const chat_selectionsString = async (prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null, voidFileService: IVoidFileService) => {
// ADD IN FILES AT TOP
const allSelections = [...currSelns || [], ...prevSelns || []]
if (allSelections.length === 0) return null
const codeSelections: CodeSelection[] = []
const fileSelections: FileSelection[] = []
const filesURIs = new Set<string>()
for (const selection of allSelections) {
if (selection.type === 'Selection') {
codeSelections.push(selection)
}
else if (selection.type === 'File') {
const fileSelection = selection
const path = fileSelection.fileURI.fsPath
if (!filesURIs.has(path)) {
filesURIs.add(path)
fileSelections.push(fileSelection)
}
}
}
const filesStr = await stringifyFileSelections(fileSelections, voidFileService)
const selnsStr = stringifyCodeSelections(codeSelections)
if (filesStr || selnsStr) return `\
ALL FILE CONTENTS
${filesStr}
${selnsStr}`
return null
}
export const chat_userMessageContentWithAllFilesToo = (userMessage: string, selectionsString: string | null) => {
if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
else return userMessage
}
export const rewriteCode_systemMessage = `\
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
Directions:
1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. ONLY output the full new file. Do not add any other explanations or text.
`
export const rewriteCode_userMessage = ({ originalCode, applyStr, uri }: { originalCode: string, applyStr: string, uri: URI }) => {
const language = filenameToVscodeLanguage(uri.fsPath) ?? ''
return `\
ORIGINAL_FILE
${tripleTick[0]}${language}
${originalCode}
${tripleTick[1]}
CHANGE
${tripleTick[0]}
${applyStr}
${tripleTick[1]}
INSTRUCTIONS
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
`
}
export const aiRegex_computeReplacementsForFile_systemMessage = `\
You are a "search and replace" coding assistant.
You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE.
The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for.
The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace.
The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes.
## Instructions
1. If you do not want to make any changes, you should respond with the word "no".
2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make.
For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name.
- Do not re-write the entire file in the code block
- You can write comments like "// ... existing code" to indicate existing code
- Make sure you give enough context in the code block to apply the changes to the correct location in the code`
export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, modelService: IModelService, voidFileService: IVoidFileService }) => {
// we may want to do this in batches
const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } }
const file = await stringifyFileSelections([fileSelection], voidFileService)
return `\
## FILE
${file}
## SEARCH_CLAUSE
Here is what the user is searching for:
${searchClause}
## REPLACE_CLAUSE
Here is what the user wants to replace it with:
${replaceClause}
## INSTRUCTIONS
Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.`
}
// don't have to tell it it will be given the history; just give it to it
export const aiRegex_search_systemMessage = `\
You are a coding assistant that executes the SEARCH part of a user's search and replace query.
You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context.
Output
- Regex query
- Files to Include (optional)
- Files to Exclude? (optional)
`
export const ORIGINAL = `<<<<<<< ORIGINAL`
export const DIVIDER = `=======`
export const FINAL = `>>>>>>> UPDATED`
export const searchReplace_systemMessage = `\
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
${tripleTick[0]}
${ORIGINAL}
// ... original code goes here
${DIVIDER}
// ... final code goes here
${FINAL}
${tripleTick[1]}
You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make.
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
Directions:
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
2. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file.
3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks.
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
- Make sure you add all necessary imports.
- Make sure the "final" code is complete and will not result in syntax/lint errors.
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
## EXAMPLE 1
ORIGINAL_FILE
${tripleTick[0]}
let w = 5
let x = 6
let y = 7
let z = 8
${tripleTick[1]}
CHANGE
Make x equal to 6.5, not 6.
${tripleTick[0]}
// ... existing code
let x = 6.5
// ... existing code
${tripleTick[1]}
## ACCEPTED OUTPUT
${tripleTick[0]}
${ORIGINAL}
let x = 6
${DIVIDER}
let x = 6.5
${FINAL}
${tripleTick[1]}
`
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
ORIGINAL_FILE
${originalCode}
CHANGE
${applyStr}
INSTRUCTIONS
Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation.
`
export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_PREFIX_SUFFIX_CHARS = 20_000
/*
a
a
a <-- final i (prefix = a\na\n)
a
|b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
c
d| <-- endLine-1 <-- initial j (moves down)
e
e <-- final j (suffix = e\ne\n)
e
e
*/
let prefix = ''
let i = startLine - 1 // 0-indexed exclusive
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
while (i !== 0) {
const newLine = fullFileLines[i - 1]
if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
prefix = `${newLine}\n${prefix}`
i -= 1
}
else break
}
let suffix = ''
let j = endLine - 1
while (j !== fullFileLines.length - 1) {
const newLine = fullFileLines[j + 1]
if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
suffix = `${suffix}\n${newLine}`
j += 1
}
else break
}
return { prefix, suffix }
}
export type QuickEditFimTagsType = {
preTag: string,
sufTag: string,
midTag: string
}
export const defaultQuickEditFimTags: QuickEditFimTagsType = {
preTag: 'ABOVE',
sufTag: 'BELOW',
midTag: 'SELECTION',
}
// this should probably be longer
export const ctrlKStream_systemMessage = ({ quickEditFIMTags: { preTag, midTag, sufTag } }: { quickEditFIMTags: QuickEditFimTagsType }) => {
return `\
You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags.
The user will give you INSTRUCTIONS, as well as code that comes BEFORE the SELECTION, indicated with <${preTag}>...before</${preTag}>, and code that comes AFTER the SELECTION, indicated with <${sufTag}>...after</${sufTag}>.
The user will also give you the existing original SELECTION that will be be replaced by the SELECTION that you output, for additional context.
Instructions:
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_code</${midTag}>. Do NOT output any text or explanations before or after this.
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
`
}
export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: {
selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string,
isOllamaFIM: false, // we require this be false for clarity
}) => {
const { preTag, sufTag, midTag } = fimTags
// prompt the model artifically on how to do FIM
// const preTag = 'BEFORE'
// const sufTag = 'AFTER'
// const midTag = 'SELECTION'
return `\
CURRENT SELECTION
${tripleTick[0]}${language}
<${midTag}>${selection}</${midTag}>
${tripleTick[1]}
INSTRUCTIONS
${instructions}
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
Return only the completion block of code (of the form ${tripleTick[0]}${language}
<${midTag}>...new code</${midTag}>
${tripleTick[1]}).`
};
@@ -8,7 +8,7 @@ import { Action2, registerAction2 } from '../../../../platform/actions/common/ac
import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IEditCodeService } from './editCodeService.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
import { roundRangeToLines } from './sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
@@ -1,77 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { QuickEdit } from './quickEditActions.js';
// service that manages state
export type VoidQuickEditState = {
quickEditsOfDocument: { [uri: string]: QuickEdit }
}
export interface IQuickEditStateService {
readonly _serviceBrand: undefined;
readonly state: VoidQuickEditState; // readonly to the user
setState(newState: Partial<VoidQuickEditState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
}
export const IQuickEditStateService = createDecorator<IQuickEditStateService>('voidQuickEditStateService');
class VoidQuickEditStateService extends Disposable implements IQuickEditStateService {
_serviceBrand: undefined;
static readonly ID = 'voidQuickEditStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
private readonly _onFocusChat = new Emitter<void>();
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
private readonly _onBlurChat = new Emitter<void>();
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
// state
state: VoidQuickEditState
constructor(
) {
super()
// initial state
this.state = { quickEditsOfDocument: {} }
}
setState(newState: Partial<VoidQuickEditState>) {
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
fireFocusChat() {
this._onFocusChat.fire()
}
fireBlurChat() {
this._onBlurChat.fire()
}
}
registerSingleton(IQuickEditStateService, VoidQuickEditStateService, InstantiationType.Eager);
@@ -74,7 +74,7 @@ function saveStylesFile() {
} catch (err) {
console.error('[scope-tailwind] Error saving styles.css:', err);
}
}, 3000);
}, 4000);
}
const args = process.argv.slice(2);
@@ -1,9 +1,12 @@
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useURIStreamState, useSettingsState } from '../util/services.js'
import { useRefState } from '../util/helpers.js'
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { usePromise, useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js'
import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { ChatMarkdownRender } from './ChatMarkdownRender.js'
enum CopyButtonText {
Idle = 'Copy',
@@ -11,6 +14,56 @@ enum CopyButtonText {
Error = 'Could not copy',
}
type IconButtonProps = {
onClick: () => void;
Icon: LucideIcon
disabled?: boolean
className?: string
}
export const IconShell1 = ({ onClick, Icon, disabled, className }: IconButtonProps) => (
<button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
e.stopPropagation();
onClick?.();
}}
className={`
size-[22px]
p-[4px]
flex items-center justify-center
text-sm bg-void-bg-3 text-void-fg-1
hover:brightness-110
border border-void-border-1 rounded
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
>
<Icon />
</button>
)
// export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
// <button
// title={title}
// disabled={disabled}
// onClick={onClick}
// className={`
// size-[24px]
// flex items-center justify-center
// text-sm
// hover:opacity-80
// disabled:opacity-50 disabled:cursor-not-allowed
// ${className}
// `}
// >
// <Icon size={16} />
// </button>
// )
const COPY_FEEDBACK_TIMEOUT = 1000 // amount of time to say 'Copied!'
const CopyButton = ({ codeStr }: { codeStr: string }) => {
@@ -27,7 +80,6 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
}, COPY_FEEDBACK_TIMEOUT)
}, [copyButtonText])
const onCopy = useCallback(() => {
clipboardService.writeText(codeStr)
.then(() => { setCopyButtonText(CopyButtonText.Copied) })
@@ -35,123 +87,257 @@ const CopyButton = ({ codeStr }: { codeStr: string }) => {
metricsService.capture('Copy Code', { length: codeStr.length }) // capture the length only
}, [metricsService, clipboardService, codeStr, setCopyButtonText])
const isSingleLine = !codeStr.includes('\n')
return <button
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
return <IconShell1
Icon={copyButtonText === CopyButtonText.Copied ? Check : copyButtonText === CopyButtonText.Error ? X : Copy}
onClick={onCopy}
>
{copyButtonText}
</button>
/>
}
// state persisted for duration of react only
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
export const ApplyBlockHoverButtons = ({ codeStr, applyBoxId }: { codeStr: string, applyBoxId: string }) => {
console.log('applyboxid', applyBoxId, applyingURIOfApplyBoxIdRef)
export const JumpToFileButton = ({ uri }: { uri: URI | 'current' }) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const jumpToFileButton = uri !== 'current' && (
<IconShell1
Icon={FileSymlink}
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
}}
/>
)
return jumpToFileButton
}
export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const metricsService = accessor.get('IMetricsService')
const [_, rerender] = useState(0)
const applyingUri = useCallback(() => applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null, [applyBoxId])
const streamState = useCallback(() => editCodeService.getURIStreamState({ uri: applyingUri() }), [editCodeService, applyingUri])
const getUriBeingApplied = useCallback(() => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}, [applyBoxId])
// listen for stream updates
useURIStreamState(
useCallback((uri, newStreamState) => {
const shouldUpdate = applyingUri()?.fsPath !== uri.fsPath
if (shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, applyingUri])
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied()
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, getUriBeingApplied])
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = (
getUriBeingApplied()?.fsPath === uri_.fsPath
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
)
if (!shouldUpdate) return
rerender(c => c + 1)
}, [applyBoxId, editCodeService, getUriBeingApplied, uri])
)
const onSubmit = useCallback(() => {
const onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (streamState() === 'streaming') return
const newApplyingUri = editCodeService.startApplying({
if (getStreamState() === 'streaming') return
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
from: 'ClickApply',
type: 'searchReplace',
applyStr: codeStr,
uri: 'current',
})
uri: uri,
startBehavior: 'keep-conflicts',
}) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptURIStreaming({ uri: newApplyingUri }) })
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isDisabled, streamState, editCodeService, codeStr, applyBoxId, metricsService])
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
const onInterrupt = useCallback(() => {
if (streamState() !== 'streaming') return
const uri = applyingUri()
if (getStreamState() !== 'streaming') return
const uri = getUriBeingApplied()
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [streamState, applyingUri, editCodeService, metricsService])
}, [getStreamState, getUriBeingApplied, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied()
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied()
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [getUriBeingApplied, editCodeService])
const onReapply = useCallback(() => {
onReject()
onClickSubmit()
}, [onReject, onClickSubmit])
const currStreamState = getStreamState()
const copyButton = (
<CopyButton codeStr={codeStr} />
)
const playButton = (
<IconShell1
Icon={Play}
onClick={onClickSubmit}
/>
)
const stopButton = (
<IconShell1
Icon={Square}
onClick={onInterrupt}
/>
)
const reapplyButton = (
<IconShell1
Icon={RotateCw}
onClick={onReapply}
/>
)
const acceptButton = (
<IconShell1
Icon={Check}
onClick={onAccept}
className="text-green-600"
/>
)
const rejectButton = (
<IconShell1
Icon={X}
onClick={onReject}
className="text-red-600"
/>
)
const isSingleLine = !codeStr.includes('\n')
const applyButton = <button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onSubmit}
>
Apply
</button>
let buttonsHTML = <></>
const stopButton = <button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={onInterrupt}
>
Stop
</button>
if (currStreamState === 'streaming') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{copyButton}
{stopButton}
</>
}
const acceptRejectButtons = <>
<button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={() => {
const uri = applyingUri()
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}}
>
Accept
</button>
<button
// btn btn-secondary btn-sm border text-sm border-vscode-input-border rounded
className={`${isSingleLine ? '' : 'px-1 py-0.5'} text-sm bg-void-bg-1 text-void-fg-1 hover:brightness-110 border border-vscode-input-border rounded`}
onClick={() => {
const uri = applyingUri()
if (uri) editCodeService.removeDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}}
>
Reject
</button>
</>
if (currStreamState === 'idle-no-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{copyButton}
{playButton}
</>
}
console.log('streamStateRef.current', streamState())
if (currStreamState === 'idle-has-changes') {
buttonsHTML = <>
<JumpToFileButton uri={uri} />
{reapplyButton}
{rejectButton}
{acceptButton}
</>
}
const statusIndicatorHTML = <div className='flex flex-row items-center min-h-4 max-h-4 min-w-4 max-w-4'>
<div
className={` size-1.5 rounded-full border
${currStreamState === 'idle-no-changes' ? 'bg-void-bg-3 border-void-border-1' :
currStreamState === 'streaming' ? 'bg-orange-500 border-orange-500 shadow-[0_0_4px_0px_rgba(234,88,12,0.6)]' :
currStreamState === 'idle-has-changes' ? 'bg-green-500 border-green-500 shadow-[0_0_4px_0px_rgba(22,163,74,0.6)]' :
'bg-void-border-1 border-void-border-1'
}`
}
/>
</div>
return {
statusIndicatorHTML,
buttonsHTML,
}
}
export const BlockCodeApplyWrapper = ({
children,
initValue,
applyBoxId,
language,
canApply,
uri,
}: {
initValue: string;
children: React.ReactNode;
applyBoxId: string;
canApply: boolean;
language: string;
uri: URI | 'current',
}) => {
const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri })
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const name = uri !== 'current' ?
<ListableToolItem
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
isSmall={true}
showDot={false}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
/>
: <span>{language}</span>
return <div className='border border-void-border-3 rounded overflow-hidden bg-void-bg-3 my-1'>
{/* header */}
<div className=" select-none flex justify-between items-center py-1 px-2 border-b border-void-border-3 cursor-default">
<div className="flex items-center">
{statusIndicatorHTML}
<span className="text-[13px] font-light text-void-fg-3">
{name}
</span>
</div>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
{buttonsHTML}
</div>
</div>
{/* contents */}
<ToolChildrenWrapper>
{children}
</ToolChildrenWrapper>
</div>
const currStreamState = streamState()
return <>
{currStreamState !== 'streaming' && <CopyButton codeStr={codeStr} />}
{currStreamState === 'idle' && !isDisabled && applyButton}
{currStreamState === 'streaming' && stopButton}
{currStreamState === 'acceptRejectAll' && acceptRejectButtons}
</>
}
@@ -1,29 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React from 'react';
import { VoidCodeEditor, VoidCodeEditorProps } from '../util/inputs.js';
export const BlockCode = ({ buttonsOnHover, ...codeEditorProps }: { buttonsOnHover?: React.ReactNode } & VoidCodeEditorProps) => {
const isSingleLine = !codeEditorProps.initValue.includes('\n')
return (
<>
<div className="relative group w-full overflow-hidden">
{buttonsOnHover === null ? null : (
<div className={`z-[1] absolute top-0 right-0 opacity-0 group-hover:opacity-100 duration-200 ${isSingleLine ? 'h-full flex items-center' : ''}`}>
<div className={`flex space-x-1 ${isSingleLine ? 'pr-2' : 'p-2'}`}>
{buttonsOnHover}
</div>
</div>
)}
<VoidCodeEditor {...codeEditorProps} />
</div>
</>
)
}
@@ -3,92 +3,199 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { JSX } from 'react'
import React, { JSX, useMemo, useState } from 'react'
import { marked, MarkedToken, Token } from 'marked'
import { BlockCode } from './BlockCode.js'
import { nameToVscodeLanguage } from '../../../helpers/detectLanguage.js'
import { ApplyBlockHoverButtons } from './ApplyBlockHoverButtons.js'
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
import { useAccessor } from '../util/services.js'
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { isAbsolute } from '../../../../../../../base/common/path.js'
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
import { BlockCode } from '../util/inputs.js'
export type ChatMessageLocation = {
threadId: string;
messageIdx: number;
}
type ApplyBoxLocation = ChatMessageLocation & { tokenIdx: string }
const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocation) => {
return `${threadId}-${messageIdx}-${tokenIdx}`
}
export const CodeSpan = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <code className={`
bg-void-bg-1
px-1
rounded-sm
font-mono font-medium
break-all
${className}
`}
>
{children}
</code>
function isValidUri(s: string): boolean {
return s.length > 5 && isAbsolute(s) && !s.includes('//') && !s.includes('/*') // common case that is a false positive is comments like //
}
const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, tokenIdx }: { token: Token | string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation, tokenIdx: string }): JSX.Element => {
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
return <code
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
onClick={onClick}
>
{text}
</code>
}
const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string, rawText: string, chatMessageLocation: ChatMessageLocation }) => {
const accessor = useAccessor()
const chatThreadService = accessor.get('IChatThreadService')
const commandService = accessor.get('ICommandService')
const editorService = accessor.get('ICodeEditorService')
const { messageIdx, threadId } = chatMessageLocation
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link = undefined
if (rawText.endsWith("`")) { // if codespan was completed
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
if (link === undefined) {
// if no link, generate link and add to cache
(chatThreadService.generateCodespanLink({ codespanStr: text, threadId })
.then(link => {
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
setDidComputeCodespanLink(true) // rerender
})
)
}
}
const onClick = () => {
if (!link) return;
const selection = link.selection
// open the file
commandService.executeCommand('vscode.open', link.uri).then(() => {
// select the text
setTimeout(() => {
if (!selection) return;
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
editor.setSelection(selection)
editor.revealRange(selection, ScrollType.Immediate)
}, 50) // needed when document was just opened and needs to initialize
})
}
return <Codespan
// text={link?.displayText || text}
text={link?.displayText || text}
onClick={onClick}
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
/>
}
export type RenderTokenOptions = { isApplyEnabled?: boolean, isLinkDetectionEnabled?: boolean }
const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ...options }: { token: Token | string, inPTag?: boolean, codeURI?: URI, chatMessageLocation?: ChatMessageLocation, tokenIdx: string, } & RenderTokenOptions): React.ReactNode => {
const accessor = useAccessor()
const languageService = accessor.get('ILanguageService')
// deal with built-in tokens first (assume marked token)
const t = token as MarkedToken
if (t.raw.trim() === '') {
return null;
}
if (t.type === "space") {
return <span>{t.raw}</span>
}
if (t.type === "code") {
const [firstLine, remainingContents] = separateOutFirstLine(t.text)
const firstLineIsURI = isValidUri(firstLine) && !codeURI
const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents
const applyBoxId = chatMessageLocationForApply ? getApplyBoxId({
threadId: chatMessageLocationForApply.threadId,
messageIdx: chatMessageLocationForApply.messageIdx,
tokenIdx: tokenIdx,
}) : null
if (!contents) return null
return <div className='my-4'>
<BlockCode
initValue={t.text}
language={t.lang === undefined ? undefined : nameToVscodeLanguage[t.lang]}
buttonsOnHover={applyBoxId && <ApplyBlockHoverButtons applyBoxId={applyBoxId} codeStr={t.text} />}
// figure out langauge and URI
let uri: URI | null
let language: string
if (codeURI) {
uri = codeURI
}
else if (firstLineIsURI) { // get lang from the uri in the first line of the markdown
uri = URI.file(firstLine)
}
else {
uri = null
}
if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined
language = convertToVscodeLang(languageService, t.lang) // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell)
}
else { // no language provided - fallback - get lang from the uri and contents
language = detectLanguage(languageService, { uri, fileContents: contents })
}
if (options.isApplyEnabled && chatMessageLocation) {
const isCodeblockClosed = t.raw.trimEnd().endsWith('```') // user should only be able to Apply when the code has been closed (t.raw ends with "```")
const applyBoxId = getApplyBoxId({
threadId: chatMessageLocation.threadId,
messageIdx: chatMessageLocation.messageIdx,
tokenIdx: tokenIdx,
})
return <BlockCodeApplyWrapper
canApply={isCodeblockClosed}
applyBoxId={applyBoxId}
initValue={contents}
language={language}
uri={uri || 'current'}
>
<BlockCode
initValue={contents}
language={language}
/>
</BlockCodeApplyWrapper>
}
return <BlockCode
initValue={contents}
language={language}
/>
</div>
}
if (t.type === "heading") {
const HeadingTag = `h${t.depth}` as keyof JSX.IntrinsicElements
const headingClasses: { [h: string]: string } = {
h1: "text-4xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h2: "text-3xl font-semibold mt-6 mb-4 pb-2 border-b border-void-bg-2",
h3: "text-2xl font-semibold mt-6 mb-4",
h4: "text-xl font-semibold mt-6 mb-4",
h5: "text-lg font-semibold mt-6 mb-4",
h6: "text-base font-semibold mt-6 mb-4 text-gray-600"
}
return <HeadingTag className={headingClasses[HeadingTag]}>{t.text}</HeadingTag>
return <HeadingTag>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
</HeadingTag>
}
if (t.type === "table") {
return (
<div className={`${noSpace ? '' : 'my-4'} overflow-x-auto`}>
<table className="min-w-full border border-void-bg-2">
<div>
<table>
<thead>
<tr className="bg-void-bg-1">
<tr>
{t.header.map((cell: any, index: number) => (
<th
key={index}
className="px-4 py-2 border border-void-bg-2 font-semibold"
style={{ textAlign: t.align[index] || "left" }}
>
<th key={index}>
{cell.raw}
</th>
))}
@@ -96,13 +203,9 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
</thead>
<tbody>
{t.rows.map((row: any[], rowIndex: number) => (
<tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
<tr key={rowIndex}>
{row.map((cell: any, cellIndex: number) => (
<td
key={cellIndex}
className="px-4 py-2 border border-void-bg-2"
style={{ textAlign: t.align[cellIndex] || "left" }}
>
<td key={cellIndex} >
{cell.raw}
</td>
))}
@@ -112,74 +215,99 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
</table>
</div>
)
// return (
// <div>
// <table className={"min-w-full border border-void-bg-2"}>
// <thead>
// <tr className="bg-void-bg-1">
// {t.header.map((cell: any, index: number) => (
// <th
// key={index}
// className="px-4 py-2 border border-void-bg-2 font-semibold"
// style={{ textAlign: t.align[index] || "left" }}
// >
// {cell.raw}
// </th>
// ))}
// </tr>
// </thead>
// <tbody>
// {t.rows.map((row: any[], rowIndex: number) => (
// <tr key={rowIndex} className={rowIndex % 2 === 0 ? 'bg-white' : 'bg-void-bg-1'}>
// {row.map((cell: any, cellIndex: number) => (
// <td
// key={cellIndex}
// className={"px-4 py-2 border border-void-bg-2"}
// style={{ textAlign: t.align[cellIndex] || "left" }}
// >
// {cell.raw}
// </td>
// ))}
// </tr>
// ))}
// </tbody>
// </table>
// </div>
// )
}
if (t.type === "hr") {
return <hr className="my-6 border-t border-void-bg-2" />
return <hr />
}
if (t.type === "blockquote") {
return <blockquote className={`pl-4 border-l-4 border-void-bg-2 italic ${noSpace ? '' : 'my-4'}`}>{t.text}</blockquote>
return <blockquote>{t.text}</blockquote>
}
if (t.type === 'list_item') {
return <li>
<input type="checkbox" checked={t.checked} readOnly />
<span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={t.text} inPTag={true} codeURI={codeURI} {...options} />
</span>
</li>
}
if (t.type === "list") {
const ListTag = t.ordered ? "ol" : "ul"
return (
<ListTag
start={t.start ? t.start : undefined}
className={`list-inside pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
>
<ListTag start={t.start ? t.start : undefined}>
{t.items.map((item, index) => (
<li key={index} className={`${noSpace ? '' : 'mb-4'}`}>
<li key={index}>
{item.task && (
<input type="checkbox" checked={item.checked} readOnly className="mr-2 form-checkbox" />
<input type="checkbox" checked={item.checked} readOnly />
)}
<span className="ml-1">
<ChatMarkdownRender chatMessageLocationForApply={chatMessageLocationForApply} string={item.text} nested={true} />
<span>
<ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} inPTag={true} {...options} />
</span>
</li>
))}
</ListTag>
)
// attempt at indentation
// return (
// <ListTag
// start={t.start ? t.start : undefined}
// className={`pl-2 ${noSpace ? '' : 'my-4'} ${t.ordered ? "list-decimal" : "list-disc"}`}
// >
// {t.items.map((item, index) => (
// <li key={index} className={`${noSpace ? '' : 'mb-2'} ml-4`}>
// {item.task && (
// <input type="checkbox" className='mr-2 form-checkbox' checked={item.checked} readOnly />
// )}
// <span className-='inline-block pr-2'>
// <ChatMarkdownRender chatMessageLocation={chatMessageLocation} string={item.text} nested={true} />
// </span>
// </li>
// ))}
// </ListTag>
// )
}
if (t.type === "paragraph") {
const contents = <>
{t.tokens.map((token, index) => (
<RenderToken key={index} token={token} tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} /> // assign a unique tokenId to nested components
<RenderToken key={index}
token={token}
tokenIdx={`${tokenIdx ? `${tokenIdx}-` : ''}${index}`} // assign a unique tokenId to inPTag components
chatMessageLocation={chatMessageLocation}
inPTag={true}
{...options}
/>
))}
</>
if (nested) return contents
return <p className={`${noSpace ? '' : 'my-4'}`}>
{contents}
</p>
if (inPTag) return <span className='block'>{contents}</span>
return <p>{contents}</p>
}
if (t.type === "html") {
return (
<p className={`${noSpace ? '' : 'my-4'}`}>
{t.raw}
</p>
)
const contents = t.raw
if (inPTag) return <span className='block'>{contents}</span>
return <p>{contents}</p>
}
if (t.type === "text" || t.type === "escape") {
@@ -193,10 +321,10 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
if (t.type === "link") {
return (
<a
className='underline'
onClick={() => { window.open(t.href) }}
href={t.href}
title={t.title ?? undefined}
className='underline cursor-pointer hover:brightness-90 transition-all duration-200'
>
{t.text}
</a>
@@ -208,25 +336,31 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
src={t.href}
alt={t.text}
title={t.title ?? undefined}
className={`max4w-full h-auto rounded ${noSpace ? '' : 'my-4'}`}
/>
}
if (t.type === "strong") {
return <strong className="font-semibold">{t.text}</strong>
return <strong>{t.text}</strong>
}
if (t.type === "em") {
return <em className="italic">{t.text}</em>
return <em>{t.text}</em>
}
// inline code
if (t.type === "codespan") {
return (
<CodeSpan>
{t.text}
</CodeSpan>
)
if (options.isLinkDetectionEnabled && chatMessageLocation) {
return <CodespanWithLink
text={t.text}
rawText={t.raw}
chatMessageLocation={chatMessageLocation}
/>
}
return <Codespan text={t.text} />
}
if (t.type === "br") {
@@ -235,24 +369,23 @@ const RenderToken = ({ token, nested, noSpace, chatMessageLocationForApply, toke
// strikethrough
if (t.type === "del") {
return <del className="line-through">{t.text}</del>
return <del>{t.text}</del>
}
// default
return (
<div className="bg-orange-50 rounded-sm overflow-hidden p-2">
<span className="text-sm text-orange-500">Unknown type:</span>
{t.raw}
<span className="text-sm text-orange-500">Unknown token rendered...</span>
</div>
)
}
export const ChatMarkdownRender = ({ string, nested = false, noSpace, chatMessageLocationForApply }: { string: string, nested?: boolean, noSpace?: boolean, chatMessageLocationForApply?: ChatMessageLocation }) => {
export const ChatMarkdownRender = ({ string, inPTag = false, chatMessageLocation, ...options }: { string: string, inPTag?: boolean, codeURI?: URI, chatMessageLocation: ChatMessageLocation | undefined } & RenderTokenOptions) => {
const tokens = marked.lexer(string); // https://marked.js.org/using_pro#renderer
return (
<>
{tokens.map((token, index) => (
<RenderToken key={index} token={token} nested={nested} noSpace={noSpace} chatMessageLocationForApply={chatMessageLocationForApply} tokenIdx={index + ''} />
<RenderToken key={index} token={token} inPTag={inPTag} chatMessageLocation={chatMessageLocation} tokenIdx={index + ''} {...options} />
))}
</>
)
@@ -3,12 +3,11 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { FormEvent, useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useSidebarState, useChatThreadsState, useQuickEditState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js';
import React, { useCallback, useEffect, useRef, useState } from 'react';
import { useSettingsState, useAccessor, useCtrlKZoneStreamingState } from '../util/services.js';
import { TextAreaFns, VoidInputBox2 } from '../util/inputs.js';
import { QuickEditPropsType } from '../../../quickEditActions.js';
import { ButtonStop, ButtonSubmit, IconX, VoidChatArea } from '../sidebar-tsx/SidebarChat.js';
import { ModelDropdown } from '../void-settings-tsx/ModelDropdown.js';
import { VOID_CTRL_K_ACTION_ID } from '../../../actionIDs.js';
import { useRefState } from '../util/helpers.js';
import { useScrollbarStyles } from '../util/useScrollbarStyles.js';
@@ -55,17 +54,24 @@ export const QuickEditChat = ({
setIsStreamingRef(isStreaming)
}, [diffareaid, setIsStreamingRef]))
const loadingIcon = <div
className="@@codicon @@codicon-loading @@codicon-modifier-spin @@codicon-no-default-spin text-void-fg-3"
/>
const onSubmit = useCallback(() => {
const onSubmit = useCallback(async () => {
if (isDisabled) return
if (isStreamingRef.current) return
textAreaFnsRef.current?.disable()
editCodeService.startApplying({
const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({
from: 'QuickEdit',
type: 'rewrite',
diffareaid,
})
startBehavior: 'keep-conflicts',
}) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
}, [isStreamingRef, isDisabled, editCodeService, diffareaid])
const onInterrupt = useCallback(() => {
@@ -87,14 +93,14 @@ export const QuickEditChat = ({
const chatAreaRef = useRef<HTMLDivElement | null>(null)
return <div ref={sizerRef} style={{ maxWidth: 450 }} className={`py-2 w-full`}>
<VoidChatArea
featureName='Ctrl+K'
divRef={chatAreaRef}
onSubmit={onSubmit}
onAbort={onInterrupt}
onClose={onX}
isStreaming={isStreamingRef.current}
loadingIcon={loadingIcon}
isDisabled={isDisabled}
featureName="Ctrl+K"
className="py-2 w-full"
onClickAnywhere={() => { textAreaRef.current?.focus() }}
>
<VoidInputBox2
@@ -5,8 +5,8 @@
import React, { useEffect, useState } from 'react';
import { AlertCircle, ChevronDown, ChevronUp, X } from 'lucide-react';
import { errorDetails } from '../../../../../../../workbench/contrib/void/common/llmMessageTypes.js';
import { useSettingsState } from '../util/services.js';
import { errorDetails } from '../../../../common/sendLLMMessageTypes.js';
export const ErrorDisplay = ({
@@ -2,11 +2,6 @@
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { mountFnGenerator } from '../util/mountFnGenerator.js'
// import { SidebarSettings } from './SidebarSettings.js';
import { useIsDark, useSidebarState } from '../util/services.js';
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
@@ -20,9 +15,9 @@ export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
const { currentTab: tab } = sidebarState
// const isDark = useIsDark()
const isDark = useIsDark()
return <div
className={`@@void-scope`} // ${isDark ? 'dark' : ''}
className={`@@void-scope ${isDark ? 'dark' : ''}`}
style={{ width: '100%', height: '100%' }}
>
<div
File diff suppressed because it is too large Load Diff
@@ -29,11 +29,11 @@ export const SidebarThreadSelector = () => {
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => allThreads![threadId1].lastModified > allThreads![threadId2].lastModified ? -1 : 1)
.filter(threadId => allThreads![threadId].messages.length !== 0)
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return (
<div className="flex p-2 flex-col gap-y-1 max-h-[400px] overflow-y-auto">
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
<div className="w-full relative flex justify-center items-center">
{/* title */}
@@ -63,14 +63,16 @@ export const SidebarThreadSelector = () => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex(
(msg) => msg.role !== 'system' && msg.role !== 'tool' && !!msg.displayContent
);
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role !== 'tool' && msg.role !== 'tool_request');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
@@ -88,9 +90,7 @@ export const SidebarThreadSelector = () => {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter(
(msg) => msg.role !== 'system'
).length;
const numMessages = pastThread.messages.filter((msg) => msg.role !== 'tool_request').length;
return (
<li key={pastThread.id}>
@@ -106,7 +106,7 @@ export const SidebarThreadSelector = () => {
`}
onClick={() => chatThreadsService.switchToThread(pastThread.id)}
onDoubleClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
title={new Date(pastThread.createdAt).toLocaleString()}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
@@ -7,6 +7,30 @@
@tailwind components;
@tailwind utilities;
& {
--void-bg-1: var(--vscode-input-background);
--void-bg-1-alt: var(--vscode-badge-background);
--void-bg-2: var(--vscode-sideBar-background);
--void-bg-2-alt: color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%);
--void-bg-3: var(--vscode-editor-background);
--void-fg-0: color-mix(in srgb, var(--vscode-tab-activeForeground) 90%, black 10%);
--void-fg-1: var(--vscode-editor-foreground);
--void-fg-2: var(--vscode-input-foreground);
--void-fg-3: var(--vscode-input-placeholderForeground);
/* --void-fg-4: var(--vscode-tab-inactiveForeground); */
--void-fg-4: var(--vscode-list-deemphasizedForeground);
--void-warning: var(--vscode-charts-yellow);
--void-border-1: var(--vscode-commandCenter-activeBorder);
--void-border-2: var(--vscode-commandCenter-border);
--void-border-3: var(--vscode-commandCenter-inactiveBorder);
--void-border-4: var(--vscode-editorGroup-border);
--void-ring-color: #007FD4;
--void-link-color: #007FD4;
}
.select-child-restyle select {
text-overflow: ellipsis;
@@ -14,6 +38,10 @@
padding-right: 24px;
}
.void-force-child-placeholder-void-fg-1 ::placeholder {
color: var(--void-fg-3);
}
* {
outline: none !important;
}
@@ -1,4 +1,4 @@
import { useCallback, useRef, useState } from 'react'
import { useCallback, useEffect, useRef, useState } from 'react'
@@ -17,3 +17,12 @@ export const useRefState = <T,>(initVal: T): ReturnType<T> => {
}, [])
return [ref, setState]
}
export const usePromise = <T,>(promise: Promise<T>): T | undefined => {
const [val, setVal] = useState<T | undefined>(undefined)
useEffect(() => {
promise.then((v) => setVal(v))
}, [promise])
return val
}
@@ -152,6 +152,37 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
})
export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, className, disabled, passwordBlur, ...inputProps }: {
value: string;
onChangeValue: (value: string) => void;
placeholder: string;
className?: string;
disabled?: boolean;
passwordBlur?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) => {
return (
<input
value={value}
onChange={(e) => onChangeValue(e.target.value)}
placeholder={placeholder}
disabled={disabled}
className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
${disabled ? 'opacity-50 cursor-not-allowed' : ''}
${className}`}
style={{
...passwordBlur && { WebkitTextSecurity: 'disc' },
background: asCssVariable(inputBackground),
color: asCssVariable(inputForeground)
}}
{...inputProps}
type={undefined} // VS Code is doing some annoyingness that breaks paste if this is defined
/>
);
};
export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, placeholder, isPasswordField, multiline }: {
onChangeText: (value: string) => void;
styles?: Partial<IInputBoxStyles>,
@@ -169,7 +200,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
ctor={InputBox}
className='
bg-void-bg-1
@@[&_::placeholder]:!void-text-void-fg-3
@@void-force-child-placeholder-void-fg-1
'
propsFn={useCallback((container) => [
container,
@@ -214,27 +245,189 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
export const VoidSlider = ({
value,
onChange,
size = 'md',
disabled = false,
min = 0,
max = 7,
step = 1,
className = '',
width = 200,
}: {
value: number;
onChange: (value: number) => void;
disabled?: boolean;
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
min?: number;
max?: number;
step?: number;
className?: string;
width?: number;
}) => {
// Calculate percentage for position
const percentage = ((value - min) / (max - min)) * 100;
// Handle track click
const handleTrackClick = (e: React.MouseEvent<HTMLDivElement>) => {
if (disabled) return;
const rect = e.currentTarget.getBoundingClientRect();
const clickPosition = e.clientX - rect.left;
const trackWidth = rect.width;
// Calculate new value
const newPercentage = Math.max(0, Math.min(1, clickPosition / trackWidth));
const rawValue = min + newPercentage * (max - min);
// Special handling to ensure max value is always reachable
if (rawValue >= max - step / 2) {
onChange(max);
return;
}
// Normal step calculation
const steppedValue = Math.round((rawValue - min) / step) * step + min;
const clampedValue = Math.max(min, Math.min(max, steppedValue));
onChange(clampedValue);
};
// Helper function to handle thumb dragging that respects steps and max
const handleThumbDrag = (moveEvent: MouseEvent, track: Element) => {
if (!track) return;
const rect = (track as HTMLElement).getBoundingClientRect();
const movePosition = moveEvent.clientX - rect.left;
const trackWidth = rect.width;
// Calculate new value
const newPercentage = Math.max(0, Math.min(1, movePosition / trackWidth));
const rawValue = min + newPercentage * (max - min);
// Special handling to ensure max value is always reachable
if (rawValue >= max - step / 2) {
onChange(max);
return;
}
// Normal step calculation
const steppedValue = Math.round((rawValue - min) / step) * step + min;
const clampedValue = Math.max(min, Math.min(max, steppedValue));
onChange(clampedValue);
};
return (
<div className={`inline-flex items-center flex-shrink-0 ${className}`}>
{/* Outer container with padding to account for thumb overhang */}
<div className={`relative flex-shrink-0 ${disabled ? 'opacity-25' : ''}`}
style={{
width,
// Add horizontal padding equal to half the thumb width
// paddingLeft: thumbSizePx / 2,
// paddingRight: thumbSizePx / 2
}}>
{/* Track container with adjusted width */}
<div className="relative w-full">
{/* Invisible wider clickable area that sits above the track */}
<div
className="absolute w-full cursor-pointer"
style={{
height: '16px',
top: '50%',
transform: 'translateY(-50%)',
zIndex: 1
}}
onClick={handleTrackClick}
/>
{/* Track */}
<div
className={`relative ${size === 'xxs' ? 'h-0.5' :
size === 'xs' ? 'h-1' :
size === 'sm' ? 'h-1.5' :
size === 'sm+' ? 'h-2' : 'h-2.5'
} bg-void-bg-2 rounded-full cursor-pointer`}
onClick={handleTrackClick}
>
{/* Filled part of track */}
<div
className={`absolute left-0 ${size === 'xxs' ? 'h-0.5' :
size === 'xs' ? 'h-1' :
size === 'sm' ? 'h-1.5' :
size === 'sm+' ? 'h-2' : 'h-2.5'
} bg-void-fg-1 rounded-full`}
style={{ width: `${percentage}%` }}
/>
</div>
{/* Thumb */}
<div
className={`absolute top-1/2 transform -translate-x-1/2 -translate-y-1/2
${size === 'xxs' ? 'h-2 w-2' :
size === 'xs' ? 'h-2.5 w-2.5' :
size === 'sm' ? 'h-3 w-3' :
size === 'sm+' ? 'h-3.5 w-3.5' : 'h-4 w-4'
}
bg-void-fg-1 rounded-full shadow-md ${disabled ? 'cursor-not-allowed' : 'cursor-grab active:cursor-grabbing'}
border border-void-fg-1`}
style={{ left: `${percentage}%`, zIndex: 2 }} // Ensure thumb is above the invisible clickable area
onMouseDown={(e) => {
if (disabled) return;
const track = e.currentTarget.previousElementSibling;
const handleMouseMove = (moveEvent: MouseEvent) => {
handleThumbDrag(moveEvent, track as Element);
};
const handleMouseUp = () => {
document.removeEventListener('mousemove', handleMouseMove);
document.removeEventListener('mouseup', handleMouseUp);
document.body.style.cursor = '';
document.body.style.userSelect = '';
};
document.body.style.userSelect = 'none';
document.body.style.cursor = 'grabbing';
document.addEventListener('mousemove', handleMouseMove);
document.addEventListener('mouseup', handleMouseUp);
e.preventDefault();
}}
/>
</div>
</div>
</div>
);
};
export const VoidSwitch = ({
value,
onChange,
size = 'md',
label,
disabled = false,
}: {
value: boolean;
onChange: (value: boolean) => void;
label?: string;
disabled?: boolean;
size?: 'xs' | 'sm' | 'sm+' | 'md';
size?: 'xxs' | 'xs' | 'sm' | 'sm+' | 'md';
}) => {
return (
<label className="inline-flex items-center cursor-pointer">
<label className="inline-flex items-center">
<div
onClick={() => !disabled && onChange(!value)}
className={`
cursor-pointer
relative inline-flex items-center rounded-full transition-colors duration-200 ease-in-out
${value ? 'bg-gray-900 dark:bg-white' : 'bg-gray-200 dark:bg-gray-700'}
${value ? 'bg-zinc-900 dark:bg-white' : 'bg-white dark:bg-zinc-600'}
${disabled ? 'opacity-25' : ''}
${size === 'xxs' ? 'h-3 w-5' : ''}
${size === 'xs' ? 'h-4 w-7' : ''}
${size === 'sm' ? 'h-5 w-9' : ''}
${size === 'sm+' ? 'h-5 w-10' : ''}
@@ -243,11 +436,13 @@ export const VoidSwitch = ({
>
<span
className={`
inline-block transform rounded-full bg-white dark:bg-gray-900 shadow transition-transform duration-200 ease-in-out
inline-block transform rounded-full bg-white dark:bg-zinc-900 shadow transition-transform duration-200 ease-in-out
${size === 'xxs' ? 'h-2 w-2' : ''}
${size === 'xs' ? 'h-2.5 w-2.5' : ''}
${size === 'sm' ? 'h-3 w-3' : ''}
${size === 'sm+' ? 'h-3.5 w-3.5' : ''}
${size === 'md' ? 'h-4 w-4' : ''}
${size === 'xxs' ? (value ? 'translate-x-2.5' : 'translate-x-0.5') : ''}
${size === 'xs' ? (value ? 'translate-x-3.5' : 'translate-x-0.5') : ''}
${size === 'sm' ? (value ? 'translate-x-5' : 'translate-x-1') : ''}
${size === 'sm+' ? (value ? 'translate-x-6' : 'translate-x-1') : ''}
@@ -255,14 +450,6 @@ export const VoidSwitch = ({
`}
/>
</div>
{label && (
<span className={`
ml-3 font-medium text-gray-900 dark:text-gray-100
${size === 'xs' ? 'text-xs' : 'text-sm'}
`}>
{label}
</span>
)}
</label>
);
};
@@ -305,7 +492,7 @@ export const VoidCheckBox = ({ label, value, onClick, className }: { label: stri
export const VoidCustomDropdownBox = <T extends any>({
export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
options,
selectedOption,
onChangeOption,
@@ -316,7 +503,8 @@ export const VoidCustomDropdownBox = <T extends any>({
className,
arrowTouchesText = true,
matchInputWidth = false,
gap = 0,
gapPx = 0,
offsetPx = -6,
}: {
options: T[];
selectedOption: T | undefined;
@@ -328,7 +516,8 @@ export const VoidCustomDropdownBox = <T extends any>({
className?: string;
arrowTouchesText?: boolean;
matchInputWidth?: boolean;
gap?: number;
gapPx?: number;
offsetPx?: number;
}) => {
const [isOpen, setIsOpen] = useState(false);
const measureRef = useRef<HTMLDivElement>(null);
@@ -347,7 +536,7 @@ export const VoidCustomDropdownBox = <T extends any>({
placement: 'bottom-start',
middleware: [
offset(gap),
offset({ mainAxis: gapPx, crossAxis: offsetPx }),
flip({
boundary: document.body,
padding: 8
@@ -382,7 +571,7 @@ export const VoidCustomDropdownBox = <T extends any>({
// if the selected option is null, set the selection to the 0th option
useEffect(() => {
if (options.length === 0) return
if (selectedOption) return
if (selectedOption !== undefined) return
onChangeOption(options[0])
}, [selectedOption, onChangeOption, options])
@@ -411,7 +600,7 @@ export const VoidCustomDropdownBox = <T extends any>({
return () => document.removeEventListener('mousedown', handleClickOutside);
}, [isOpen, refs.floating, refs.reference]);
if (!selectedOption)
if (selectedOption === undefined)
return null
return (
@@ -630,8 +819,8 @@ const normalizeIndentation = (code: string): string => {
const modelOfEditorId: { [id: string]: ITextModel | undefined } = {}
export type VoidCodeEditorProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars }: VoidCodeEditorProps) => {
export type BlockCodeProps = { initValue: string, language?: string, maxHeight?: number, showScrollbars?: boolean }
export const BlockCode = ({ initValue, language, maxHeight, showScrollbars }: BlockCodeProps) => {
initValue = normalizeIndentation(initValue)
@@ -646,7 +835,6 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
// const languageDetectionService = accessor.get('ILanguageDetectionService')
const modelService = accessor.get('IModelService')
const id = useId()
// these are used to pass to the model creation of modelRef
@@ -727,9 +915,11 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
}, [instantiationService])}
onCreateInstance={useCallback((editor: CodeEditorWidget) => {
const languageId = languageRef.current ? languageRef.current : 'plaintext'
const model = modelOfEditorId[id] ?? modelService.createModel(
initValueRef.current + '\n', {
languageId: languageRef.current ? languageRef.current : 'typescript',
initValueRef.current, {
languageId: languageId,
onDidChange: (e) => { return { dispose: () => { } } } // no idea why they'd require this
})
modelRef.current = model
@@ -766,7 +956,7 @@ export const VoidCodeEditor = ({ initValue, language, maxHeight, showScrollbars
export const VoidButton = ({ children, disabled, onClick }: { children: React.ReactNode; disabled?: boolean; onClick: () => void }) => {
return <button disabled={disabled}
className='px-3 py-1 bg-black/10 dark:bg-gray-200/10 rounded-sm overflow-hidden'
className='px-3 py-1 bg-black/10 dark:bg-white/10 rounded-sm overflow-hidden whitespace-nowrap'
onClick={onClick}
>{children}</button>
}
@@ -18,9 +18,21 @@ export const mountFnGenerator = (Component: (params: any) => React.ReactNode) =>
const disposables = _registerServices(accessor)
const root = ReactDOM.createRoot(rootElement)
root.render(<Component {...props} />); // tailwind dark theme indicator
return disposables
const rerender = (props?: any) => {
root.render(<Component {...props} />); // tailwind dark theme indicator
}
const dispose = () => {
root.unmount();
disposables.forEach(d => d.dispose());
}
rerender(props)
const returnVal = {
rerender,
dispose,
}
return returnVal
}
@@ -4,14 +4,11 @@
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect, useCallback } from 'react'
import { ThreadStreamState,IChatThreadService, ThreadsState } from '../../../../common/chatThreadService.js'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { VoidUriState } from '../../../voidUriStateService.js';
import { VoidQuickEditState } from '../../../quickEditStateService.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
import { ServicesAccessor } from '../../../../../../../editor/browser/editorExtensions.js';
@@ -22,12 +19,11 @@ import { IContextViewService, IContextMenuService } from '../../../../../../../p
import { IFileService } from '../../../../../../../platform/files/common/files.js';
import { IHoverService } from '../../../../../../../platform/hover/browser/hover.js';
import { IThemeService } from '../../../../../../../platform/theme/common/themeService.js';
import { ILLMMessageService } from '../../../../../../../workbench/contrib/void/common/llmMessageService.js';
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IEditCodeService, URIStreamState } from '../../../editCodeService.js';
import { IVoidUriStateService } from '../../../voidUriStateService.js';
import { IQuickEditStateService } from '../../../quickEditStateService.js';
import { IEditCodeService } from '../../../editCodeServiceInterface.js'
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
@@ -44,18 +40,18 @@ import { IConfigurationService } from '../../../../../../../platform/configurati
import { IPathService } from '../../../../../../../workbench/services/path/common/pathService.js'
import { IMetricsService } from '../../../../../../../workbench/contrib/void/common/metricsService.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { IChatThreadService, ThreadsState, ThreadStreamState } from '../../../chatThreadService.js'
import { ITerminalToolService } from '../../../terminalToolService.js'
import { ILanguageService } from '../../../../../../../editor/common/languages/language.js'
import { IVoidModelService } from '../../../../common/voidModelService.js'
import { IWorkspaceContextService } from '../../../../../../../platform/workspace/common/workspace.js'
import { IVoidCommandBarService } from '../../../voidCommandBarService.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
let uriState: VoidUriState
const uriStateListeners: Set<(s: VoidUriState) => void> = new Set()
let quickEditState: VoidQuickEditState
const quickEditStateListeners: Set<(s: VoidQuickEditState) => void> = new Set()
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
@@ -77,54 +73,30 @@ let colorThemeState: ColorScheme
const colorThemeStateListeners: Set<(s: ColorScheme) => void> = new Set()
const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) => void> = new Set()
const uriStreamingStateListeners: Set<(uri: URI, s: URIStreamState) => void> = new Set()
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
// must call this before you can use any of the hooks below
// this should only be called ONCE! this is the only place you don't need to dispose onDidChange. If you use state.onDidChange anywhere else, make sure to dispose it!
let wasCalled = false
export const _registerServices = (accessor: ServicesAccessor) => {
const disposables: IDisposable[] = []
// don't register services twice
if (wasCalled) {
return
// console.error(`⚠️ Void _registerServices was called again! It should only be called once.`)
}
wasCalled = true
_registerAccessor(accessor)
const stateServices = {
uriStateService: accessor.get(IVoidUriStateService),
quickEditStateService: accessor.get(IQuickEditStateService),
sidebarStateService: accessor.get(ISidebarStateService),
chatThreadsStateService: accessor.get(IChatThreadService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
themeService: accessor.get(IThemeService),
editCodeService: accessor.get(IEditCodeService),
voidCommandBarService: accessor.get(IVoidCommandBarService),
modelService: accessor.get(IModelService),
}
const { uriStateService, sidebarStateService, quickEditStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService } = stateServices
uriState = uriStateService.state
disposables.push(
uriStateService.onDidChangeState(() => {
uriState = uriStateService.state
uriStateListeners.forEach(l => l(uriState))
})
)
quickEditState = quickEditStateService.state
disposables.push(
quickEditStateService.onDidChangeState(() => {
quickEditState = quickEditStateService.state
quickEditStateListeners.forEach(l => l(quickEditState))
})
)
const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
sidebarState = sidebarStateService.state
disposables.push(
@@ -170,7 +142,7 @@ export const _registerServices = (accessor: ServicesAccessor) => {
colorThemeState = themeService.getColorTheme().type
disposables.push(
themeService.onDidColorThemeChange(theme => {
themeService.onDidColorThemeChange(({ theme }) => {
colorThemeState = theme.type
colorThemeStateListeners.forEach(l => l(colorThemeState))
})
@@ -178,15 +150,21 @@ export const _registerServices = (accessor: ServicesAccessor) => {
// no state
disposables.push(
editCodeService.onDidChangeCtrlKZoneStreaming(({ diffareaid }) => {
editCodeService.onDidChangeStreamingInCtrlKZone(({ diffareaid }) => {
const isStreaming = editCodeService.isCtrlKZoneStreaming({ diffareaid })
ctrlKZoneStreamingStateListeners.forEach(l => l(diffareaid, isStreaming))
})
)
disposables.push(
editCodeService.onDidChangeURIStreamState(({ uri }) => {
const isStreaming = editCodeService.getURIStreamState({ uri })
uriStreamingStateListeners.forEach(l => l(uri, isStreaming))
voidCommandBarService.onDidChangeState(({ uri }) => {
commandBarURIStateListeners.forEach(l => l(uri));
})
)
disposables.push(
voidCommandBarService.onDidChangeActiveURI(({ uri }) => {
activeURIListeners.forEach(l => l(uri));
})
)
@@ -210,8 +188,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IEditCodeService: accessor.get(IEditCodeService),
IVoidUriStateService: accessor.get(IVoidUriStateService),
IQuickEditStateService: accessor.get(IQuickEditStateService),
ISidebarStateService: accessor.get(ISidebarStateService),
IChatThreadService: accessor.get(IChatThreadService),
@@ -231,6 +207,12 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IConfigurationService: accessor.get(IConfigurationService),
IPathService: accessor.get(IPathService),
IMetricsService: accessor.get(IMetricsService),
ITerminalToolService: accessor.get(ITerminalToolService),
ILanguageService: accessor.get(ILanguageService),
IVoidModelService: accessor.get(IVoidModelService),
IWorkspaceContextService: accessor.get(IWorkspaceContextService),
IVoidCommandBarService: accessor.get(IVoidCommandBarService),
} as const
return reactAccessor
@@ -258,26 +240,6 @@ export const useAccessor = () => {
// -- state of services --
export const useUriState = () => {
const [s, ss] = useState(uriState)
useEffect(() => {
ss(uriState)
uriStateListeners.add(ss)
return () => { uriStateListeners.delete(ss) }
}, [ss])
return s
}
export const useQuickEditState = () => {
const [s, ss] = useState(quickEditState)
useEffect(() => {
ss(quickEditState)
quickEditStateListeners.add(ss)
return () => { quickEditStateListeners.delete(ss) }
}, [ss])
return s
}
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {
@@ -364,14 +326,6 @@ export const useCtrlKZoneStreamingState = (listener: (diffareaid: number, s: boo
}, [listener, ctrlKZoneStreamingStateListeners])
}
export const useURIStreamState = (listener: (uri: URI, s: URIStreamState) => void) => {
useEffect(() => {
uriStreamingStateListeners.add(listener)
return () => { uriStreamingStateListeners.delete(listener) }
}, [listener, uriStreamingStateListeners])
}
export const useIsDark = () => {
const [s, ss] = useState(colorThemeState)
useEffect(() => {
@@ -383,6 +337,40 @@ export const useIsDark = () => {
// s is the theme, return isDark instead of s
const isDark = s === ColorScheme.DARK || s === ColorScheme.HIGH_CONTRAST_DARK
return isDark
}
export const useCommandBarURIListener = (listener: (uri: URI) => void) => {
useEffect(() => {
commandBarURIStateListeners.add(listener);
return () => { commandBarURIStateListeners.delete(listener) };
}, [listener]);
};
export const useCommandBarState = () => {
const accessor = useAccessor()
const commandBarService = accessor.get('IVoidCommandBarService')
const [s, ss] = useState({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
const listener = useCallback(() => {
ss({ state: commandBarService.stateOfURI, sortedURIs: commandBarService.sortedURIs });
}, [commandBarService])
useCommandBarURIListener(listener)
return s;
}
// roughly gets the active URI - this is used to get the history of recent URIs
export const useActiveURI = () => {
const accessor = useAccessor()
const commandBarService = accessor.get('IVoidCommandBarService')
const [s, ss] = useState(commandBarService.activeURI)
useEffect(() => {
const listener = () => { ss(commandBarService.activeURI) }
activeURIListeners.add(listener);
return () => { activeURIListeners.delete(listener) };
}, [])
return { uri: s }
}
@@ -0,0 +1,307 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js';
import '../styles.css'
import { useCallback, useEffect, useState, useRef } from 'react';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const isDark = useIsDark()
return <div
className={`@@void-scope ${isDark ? 'dark' : ''}`}
>
<VoidCommandBar uri={uri} editor={editor} />
</div>
}
const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => {
if (len === 0) return null
return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset
}
const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const editorService = accessor.get('ICodeEditorService')
const metricsService = accessor.get('IMetricsService')
const commandService = accessor.get('ICommandService')
const commandBarService = accessor.get('IVoidCommandBarService')
const voidModelService = accessor.get('IVoidModelService')
const { state: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
// useEffect(() => {
// console.log('MOUNTING!!!')
// }, [])
// latestUriIdx is used to remember place in leftRight
const _latestValidUriIdxRef = useRef<number | null>(null)
// i is the current index of the URI in sortedCommandBarURIs
const i_ = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath)
const currFileIdx = i_ === -1 ? null : i_
useEffect(() => {
if (currFileIdx !== null) _latestValidUriIdxRef.current = currFileIdx
}, [currFileIdx])
const uriIdxInStepper = currFileIdx !== null ? currFileIdx // use currFileIdx if it exists, else use latestNotNullUriIdxRef
: _latestValidUriIdxRef.current === null ? null
: _latestValidUriIdxRef.current < sortedCommandBarURIs.length ? _latestValidUriIdxRef.current
: null
// when change URI, scroll to the proper spot
useEffect(() => {
setTimeout(() => {
// check undefined
if (!uri) return
const s = commandBarService.stateOfURI[uri.fsPath]
if (!s) return
const { diffIdx } = s
goToDiffIdx(diffIdx ?? 0)
}, 50)
}, [uri, commandBarService])
if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files
const getNextDiffIdx = (step: 1 | -1) => {
// check undefined
if (!uri) return null
const s = commandBarState[uri.fsPath]
if (!s) return null
const { diffIdx, sortedDiffIds } = s
// get next idx
const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step)
return nextDiffIdx
}
const goToDiffIdx = (idx: number | null) => {
if (idx === null) return
// check undefined
if (!uri) return
const s = commandBarState[uri.fsPath]
if (!s) return
const { sortedDiffIds } = s
// reveal
const diffid = sortedDiffIds[idx]
if (diffid === undefined) return
const diff = editCodeService.diffOfId[diffid]
if (!diff) return
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
commandBarService.setDiffIdx(uri, idx)
}
const getNextUriIdx = (step: 1 | -1) => {
return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step)
}
const goToURIIdx = async (idx: number | null) => {
if (idx === null) return
const nextURI = sortedCommandBarURIs[idx]
editCodeService.diffAreasOfURI
const { model } = await voidModelService.getModelSafe(nextURI)
if (model) {
// switch to the URI
editorService.openCodeEditor({ resource: nextURI, options: { revealIfVisible: true } }, editor)
}
}
const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null
const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : []
const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : []
const isADiffInThisFile = sortedDiffIds.length !== 0
const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0
const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0
const streamState = uri ? commandBarService.getStreamState(uri) : null
const showAcceptRejectAll = streamState === 'idle-has-changes'
const nextDiffIdx = getNextDiffIdx(1)
const prevDiffIdx = getNextDiffIdx(-1)
const nextURIIdx = getNextUriIdx(1)
const prevURIIdx = getNextUriIdx(-1)
const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile)
const upButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}// --border border-void-border-3 focus:border-void-border-1
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(prevDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(prevDiffIdx);
}
}}
></button>
const downButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(nextDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(nextDiffIdx);
}
}}
></button>
const leftButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(prevURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(prevURIIdx);
}
}}
></button>
const rightButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(nextURIIdx);
}
}}
></button>
// accept/reject if current URI has changes
const onAcceptAll = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Accept All', {})
}
const onRejectAll = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Reject All', {})
}
if (!isADiffZoneInAnyFile) return null
const acceptAllButton = <button
className='text-nowrap'
onClick={onAcceptAll}
style={{
backgroundColor: acceptAllBg,
border: acceptBorder,
color: buttonTextColor,
fontSize: buttonFontSize,
padding: '2px 4px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Accept File
</button>
const rejectAllButton = <button
className='text-nowrap'
onClick={onRejectAll}
style={{
backgroundColor: rejectAllBg,
border: rejectBorder,
color: 'white',
fontSize: buttonFontSize,
padding: '2px 4px',
borderRadius: '6px',
cursor: 'pointer'
}}
>
Reject File
</button>
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
{acceptAllButton}
{rejectAllButton}
</div>
// const closeCommandBar = useCallback(() => {
// commandService.executeCommand('void.hideCommandBar');
// }, [commandService]);
// const hideButton = <button
// className='ml-auto pointer-events-auto'
// onClick={closeCommandBar}
// style={{
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// title="Close command bar"
// >x
// </button>
const leftRightUpDownButtons = <div className='p-1 gap-1 flex flex-col items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 w-full'>
<div className="flex flex-col gap-1">
{/* Changes in file */}
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
{upButton}
{downButton}
<span className="min-w-16 px-2 text-xs">
{isADiffInThisFile ?
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming' ?
'No changes yet'
: `No changes`
}
</span>
</div>
{/* Files */}
<div className={`${!isADiffZoneInAnyFile ? 'hidden' : ''} flex items-center ${leftRightDisabled ? 'opacity-50' : ''}`}>
{leftButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
{rightButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
<span className="min-w-16 px-2 text-xs">
{currFileIdx !== null ?
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
}
</span>
</div>
</div>
</div>
return <div className={`flex flex-col items-center gap-y-2 mx-2 pointer-events-auto`}>
{showAcceptRejectAll && acceptRejectAllButtons}
{leftRightUpDownButtons}
</div>
}
@@ -0,0 +1,9 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { mountFnGenerator } from '../util/mountFnGenerator.js'
import { VoidCommandBarMain } from './VoidCommandBar.js'
export const mountVoidCommandBar = mountFnGenerator(VoidCommandBarMain)
@@ -10,7 +10,7 @@ import { _VoidSelectBox, VoidCustomDropdownBox } from '../util/inputs.js'
import { SelectBox } from '../../../../../../../base/browser/ui/selectBox/selectBox.js'
import { IconWarning } from '../sidebar-tsx/SidebarChat.js'
import { VOID_OPEN_SETTINGS_ACTION_ID, VOID_TOGGLE_SETTINGS_ACTION_ID } from '../../../voidSettingsPane.js'
import { ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { modelFilterOfFeatureName, ModelOption } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { WarningBox } from './WarningBox.js'
const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
@@ -21,7 +21,7 @@ const optionsEqual = (m1: ModelOption[], m2: ModelOption[]) => {
return true
}
const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], featureName: FeatureName }) => {
const ModelSelectBox = ({ options, featureName, className }: { options: ModelOption[], featureName: FeatureName, className: string }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
@@ -38,9 +38,9 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
onChangeOption={onChangeOption}
getOptionDisplayName={(option) => option.selection.modelName}
getOptionDropdownName={(option) => option.selection.modelName}
getOptionDropdownDetail={(option) => option.selection.providerName }
getOptionDropdownDetail={(option) => option.selection.providerName}
getOptionsEqual={(a, b) => optionsEqual([a], [b])}
className='text-xs text-void-fg-3 px-1'
className={className}
matchInputWidth={false}
/>
}
@@ -77,25 +77,32 @@ const ModelSelectBox = ({ options, featureName }: { options: ModelOption[], feat
const MemoizedModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
const MemoizedModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => {
const settingsState = useSettingsState()
const oldOptionsRef = useRef<ModelOption[]>([])
const [memoizedOptions, setMemoizedOptions] = useState(oldOptionsRef.current)
const { filter, emptyMessage } = modelFilterOfFeatureName[featureName]
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode }))
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
}
oldOptionsRef.current = newOptions
}, [settingsState._modelOptions])
}, [settingsState._modelOptions, filter])
return <ModelSelectBox featureName={featureName} options={memoizedOptions} />
if (memoizedOptions.length === 0) { // Pretty sure this will never be reached unless filter is enabled
return <WarningBox text={emptyMessage?.message || 'No models available'} />
}
return <ModelSelectBox featureName={featureName} options={memoizedOptions} className={className} />
}
export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) => {
export const ModelDropdown = ({ featureName, className }: { featureName: FeatureName, className: string }) => {
const settingsState = useSettingsState()
const accessor = useAccessor()
@@ -103,14 +110,18 @@ export const ModelDropdown = ({ featureName }: { featureName: FeatureName }) =>
const openSettings = () => { commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID); };
const { emptyMessage } = modelFilterOfFeatureName[featureName]
const isDisabled = isFeatureNameDisabled(featureName, settingsState)
if (isDisabled)
return <WarningBox onClick={openSettings} text={
isDisabled === 'needToEnableModel' ? 'Enable a model'
: isDisabled === 'addModel' ? 'Add a model'
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
: 'Provider required'
emptyMessage && emptyMessage.priority === 'always' ? emptyMessage.message :
isDisabled === 'needToEnableModel' ? 'Enable a model'
: isDisabled === 'addModel' ? 'Add a model'
: (isDisabled === 'addProvider' || isDisabled === 'notFilledIn' || isDisabled === 'providerNotAutoDetected') ? 'Provider required'
: 'Provider required'
} />
return <MemoizedModelDropdown featureName={featureName} />
return <MemoizedModelDropdown featureName={featureName} className={className} />
}
@@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useRef, useState } from 'react'
import { InputBox } from '../../../../../../../base/browser/ui/inputbox/inputBox.js'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidModelInfo, globalSettingNames, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, defaultProviderSettings, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSwitch } from '../util/inputs.js'
import { VoidButton, VoidCheckBox, VoidCustomDropdownBox, VoidInputBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, MoveRight } from 'lucide-react'
import { useScrollbarStyles } from '../util/useScrollbarStyles.js'
@@ -17,14 +17,13 @@ import { env } from '../../../../../../../base/common/process.js'
import { ModelDropdown } from './ModelDropdown.js'
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js'
import { WarningBox } from './WarningBox.js'
import { os } from '../../../helpers/systemInfo.js'
import { os } from '../../../../common/helpers/systemInfo.js'
import { IconX } from '../sidebar-tsx/SidebarChat.js'
const SubtleButton = ({ onClick, text, icon, disabled }: { onClick: () => void, text: string, icon: React.ReactNode, disabled: boolean }) => {
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
return <div className='flex items-center text-void-fg-3 px-3 py-0.5 rounded-sm overflow-hidden gap-2 hover:bg-black/10 dark:hover:bg-gray-300/10'>
<button className='flex items-center' disabled={disabled} onClick={onClick}>
{icon}
</button>
{leftButton ? leftButton : null}
<span>
{text}
</span>
@@ -57,22 +56,28 @@ const RefreshModelButton = ({ providerName }: { providerName: RefreshableProvide
const { state } = refreshModelState[providerName]
const { title: providerTitle } = displayInfoOfProviderName(providerName)
return <SubtleButton
onClick={() => {
refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false })
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
}}
text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!`
: justFinished === 'error' ? `${providerTitle} not found!`
: `Manually refresh ${providerTitle} models.`
}
icon={justFinished === 'finished' ? <Check className='stroke-green-500 size-3' />
: justFinished === 'error' ? <X className='stroke-red-500 size-3' />
: state === 'refreshing' ? <Loader2 className='size-3 animate-spin' />
: <RefreshCw className='size-3' />
return <ButtonLeftTextRightOption
leftButton={
<button
className='flex items-center'
disabled={state === 'refreshing' || justFinished !== null}
onClick={() => {
refreshModelService.startRefreshingModels(providerName, { enableProviderOnSuccess: false, doNotFire: false })
metricsService.capture('Click', { providerName, action: 'Refresh Models' })
}}
>
{justFinished === 'finished' ? <Check className='stroke-green-500 size-3' />
: justFinished === 'error' ? <X className='stroke-red-500 size-3' />
: state === 'refreshing' ? <Loader2 className='size-3 animate-spin' />
: <RefreshCw className='size-3' />}
</button>
}
disabled={state === 'refreshing' || justFinished !== null}
text={justFinished === 'finished' ? `${providerTitle} Models are up-to-date!`
: justFinished === 'error' ? `${providerTitle} not found!`
: `Manually refresh ${providerTitle} models.`}
/>
}
@@ -93,7 +98,7 @@ const RefreshableModels = () => {
const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const AddModelMenu = ({ onSubmit, onClose }: { onSubmit: () => void, onClose: () => void }) => {
const accessor = useAccessor()
const settingsStateService = accessor.get('IVoidSettingsService')
@@ -116,8 +121,8 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
options={providerNames}
selectedOption={providerName}
onChangeOption={(pn) => setProviderName(pn)}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : '(null)'}
getOptionDisplayName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionDropdownName={(pn) => pn ? displayInfoOfProviderName(pn).title : 'Provider Name'}
getOptionsEqual={(a, b) => a === b}
className={`max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root
py-[4px] px-[6px]
@@ -141,8 +146,8 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
</div>
{/* button */}
<div className='max-w-40'>
<VoidButton onClick={() => {
<VoidButton
onClick={() => {
const modelName = modelNameRef.current?.value
if (providerName === null) {
@@ -161,16 +166,16 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
settingsStateService.addModel(providerName, modelName)
onSubmit()
}}
>Add model</VoidButton>
</div>
>Add model</VoidButton>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap'>
{errorString}
</div>}
<button onClick={onClose} className='ml-auto'><X className='size-4' /></button>
</div>
{!errorString ? null : <div className='text-red-500 truncate whitespace-nowrap mt-1'>
{errorString}
</div>}
</>
}
@@ -178,9 +183,9 @@ const AddModelMenu = ({ onSubmit }: { onSubmit: () => void }) => {
const AddModelMenuFull = () => {
const [open, setOpen] = useState(false)
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 pb-1 px-3 rounded-sm overflow-hidden '>
return <div className='hover:bg-black/10 dark:hover:bg-gray-300/10 py-1 my-4 px-3 rounded-sm overflow-hidden'>
{open ?
<AddModelMenu onSubmit={() => { setOpen(false) }} />
<AddModelMenu onSubmit={() => setOpen(false)} onClose={() => setOpen(false)} />
: <VoidButton onClick={() => setOpen(true)}>Add Model</VoidButton>
}
</div>
@@ -230,9 +235,7 @@ export const ModelDump = () => {
<VoidSwitch
value={disabled ? false : !isHidden}
onChange={() => {
settingsStateService.toggleModelHidden(providerName, modelName)
}}
onChange={() => { settingsStateService.toggleModelHidden(providerName, modelName) }}
disabled={disabled}
size='sm'
/>
@@ -252,48 +255,32 @@ export const ModelDump = () => {
const ProviderSetting = ({ providerName, settingName }: { providerName: ProviderName, settingName: SettingName }) => {
// const { title: providerTitle, } = displayInfoOfProviderName(providerName)
const { title: settingTitle, placeholder, isPasswordField, subTextMd } = displayInfoOfSettingName(providerName, settingName)
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const settingsState = useSettingsState()
let weChangedTextRef = false
const settingValue = settingsState.settingsOfProvider[providerName][settingName] as string // this should always be a string in this component
console.log(`providerName:${providerName} settingName: ${settingName}, settingValue: ${settingValue}`)
if (typeof settingValue !== 'string') {
console.log('Error: Provider setting had a non-string value.')
return
}
return <ErrorBoundary>
<div className='my-1'>
<VoidInputBox
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
placeholder={`${settingTitle} (${placeholder})`}
onChangeText={useCallback((newVal) => {
if (weChangedTextRef) return
<VoidSimpleInputBox
value={settingValue}
onChangeValue={useCallback((newVal) => {
voidSettingsService.setSettingOfProvider(providerName, settingName, newVal)
}, [voidSettingsService, providerName, settingName])}
// we are responsible for setting the initial value. always sync the instance whenever there's a change to state.
onCreateInstance={useCallback((instance: InputBox) => {
const syncInstance = () => {
const settingsAtProvider = voidSettingsService.state.settingsOfProvider[providerName];
const stateVal = settingsAtProvider[settingName as SettingName]
// console.log('SYNCING TO', providerName, settingName, stateVal)
weChangedTextRef = true
instance.value = stateVal as string
weChangedTextRef = false
}
syncInstance()
const disposable = voidSettingsService.onDidChangeState(syncInstance)
return [disposable]
}, [voidSettingsService, providerName, settingName])}
multiline={false}
isPasswordField={isPasswordField}
// placeholder={`${providerTitle} ${settingTitle} (${placeholder})`}
placeholder={`${settingTitle} (${placeholder})`}
passwordBlur={isPasswordField}
/>
{subTextMd === undefined ? null : <div className='py-1 px-3 opacity-50 text-sm'>
<ChatMarkdownRender noSpace string={subTextMd} />
<ChatMarkdownRender string={subTextMd} chatMessageLocation={undefined} />
</div>}
</div>
@@ -356,7 +343,7 @@ export const VoidProviderSettings = ({ providerNames }: { providerNames: Provide
type TabName = 'models' | 'general'
export const AutoRefreshToggle = () => {
export const AutoDetectLocalModelsToggle = () => {
const settingName: GlobalSettingName = 'autoRefreshModels'
const accessor = useAccessor()
@@ -368,16 +355,19 @@ export const AutoRefreshToggle = () => {
// right now this is just `enabled_autoRefreshModels`
const enabled = voidSettingsState.globalSettings[settingName]
return <SubtleButton
onClick={() => {
voidSettingsService.setGlobalSetting(settingName, !enabled)
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled })
}}
return <ButtonLeftTextRightOption
leftButton={<VoidSwitch
size='xxs'
value={enabled}
onChange={(newVal) => {
voidSettingsService.setGlobalSetting(settingName, newVal)
metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: newVal })
}}
/>}
text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`}
icon={enabled ? <Check className='stroke-green-500 size-3' /> : <X className='stroke-red-500 size-3' />}
disabled={false}
/>
}
export const AIInstructionsBox = () => {
@@ -395,15 +385,43 @@ export const AIInstructionsBox = () => {
/>
}
const FastApplyMethodDropdown = () => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const options = useMemo(() => [true, false], [])
const onChangeOption = useCallback((newVal: boolean) => {
voidSettingsService.setGlobalSetting('enableFastApply', newVal)
}, [voidSettingsService])
return <VoidCustomDropdownBox
className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1'
options={options}
selectedOption={voidSettingsService.state.globalSettings.enableFastApply}
onChangeOption={onChangeOption}
getOptionDisplayName={(val) => val ? 'Fast Apply' : 'Slow Apply'}
getOptionDropdownName={(val) => val ? 'Fast Apply' : 'Slow Apply'}
getOptionDropdownDetail={(val) => val ? 'Output Search/Replace blocks' : 'Rewrite whole files'}
getOptionsEqual={(a, b) => a === b}
/>
}
export const FeaturesTab = () => {
const voidSettingsState = useSettingsState()
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
return <>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ErrorBoundary>
<AutoRefreshToggle />
<RefreshableModels />
<div className='py-2' />
<ModelDump />
<AddModelMenuFull />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
@@ -412,12 +430,12 @@ export const FeaturesTab = () => {
{/* <h3 className={`opacity-50 mb-2`}>{`Instructions:`}</h3> */}
{/* <h3 className={`mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3> */}
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='pl-4 opacity-50'>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`1. Download [Ollama](https://ollama.com/download).`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`2. Open your terminal.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender noSpace string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender noSpace string={`Void automatically detects locally running models and enables them.`} /></span>
<div className='pl-4 prose-ol:list-decimal opacity-80'>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`1. Download [Ollama](https://ollama.com/download).`} chatMessageLocation={undefined} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`2. Open your terminal.`} chatMessageLocation={undefined} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`3. Run \`ollama run llama3.1:8b\`. This installs Meta's llama3.1 model which is best for chat and inline edits. Requires 5GB of memory.`} chatMessageLocation={undefined} /></span>
<span className={`text-sm mb-2 select-text`}><ChatMarkdownRender string={`4. Run \`ollama run qwen2.5-coder:1.5b\`. This installs a faster autocomplete model. Requires 1GB of memory.`} chatMessageLocation={undefined} /></span>
<span className={`text-sm mb-2`}><ChatMarkdownRender string={`Void automatically detects locally running models and enables them.`} chatMessageLocation={undefined} /></span>
{/* TODO we should create UI for downloading models without user going into terminal */}
</div>
@@ -434,17 +452,96 @@ export const FeaturesTab = () => {
<h2 className={`text-3xl mb-2 mt-12`}>Feature Options</h2>
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
<ErrorBoundary>
{featureNames.map(featureName =>
(['Ctrl+L', 'Ctrl+K'] as FeatureName[]).includes(featureName) ? null :
<div key={featureName}
className='mb-2'
>
<h4 className={`text-void-fg-3`}>{displayInfoOfFeatureName(featureName)}</h4>
<ModelDropdown featureName={featureName} />
{/* L1 */}
<div className='flex items-start justify-around mt-4 my-4 gap-x-8'>
{/* FIM */}
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Experimental. Only works with models that support FIM.</div>
<div className='my-2'>
{/* Enable Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.enableAutocomplete}
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${!voidSettingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
)}
</div>
{/* Apply */}
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button and the Edit tool.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.syncApplyToChat}
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${voidSettingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
<div className='my-2'>
{/* Fast Apply Method Dropdown */}
<div className='flex items-center gap-x-2 my-2'>
<FastApplyMethodDropdown />
</div>
</div>
</div>
</div>
{/* L2 */}
<div className='flex items-start justify-around my-4 gap-x-8'>
{/* Tools Section */}
<div className='w-full'>
<h4 className={`text-base`}>Tools</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={voidSettingsState.globalSettings.autoApprove}
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoApprove', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{voidSettingsState.globalSettings.autoApprove ? 'Auto-approve' : 'Auto-approve'}</span>
</div>
</div>
</div>
<div className='w-full'>
</div>
</div>
<div className='py-8' />
</ErrorBoundary>
</>
@@ -649,7 +746,7 @@ export const Settings = () => {
{/* content */}
<div className='w-full min-w-[600px] overflow-auto'>
<div className='w-full min-w-[550px]'>
<div className={`${tab !== 'models' ? 'hidden' : ''}`}>
<FeaturesTab />
@@ -9,6 +9,28 @@ module.exports = {
content: ['./src2/**/*.{jsx,tsx}'], // uses these files to decide how to transform the css file
theme: {
extend: {
typography: theme => ({
DEFAULT: {
css: {
'--tw-prose-body': 'var(--void-fg-1)',
'--tw-prose-headings': 'var(--void-fg-1)',
'--tw-prose-lead': 'var(--void-fg-2)',
'--tw-prose-links': 'var(--void-link-color)',
'--tw-prose-bold': 'var(--void-fg-1)',
'--tw-prose-counters': 'var(--void-fg-3)',
'--tw-prose-bullets': 'var(--void-fg-3)',
'--tw-prose-hr': 'var(--void-border-4)',
'--tw-prose-quotes': 'var(--void-fg-1)',
'--tw-prose-quote-borders': 'var(--void-border-2)',
'--tw-prose-captions': 'var(--void-fg-3)',
'--tw-prose-code': 'var(--void-fg-0)',
'--tw-prose-pre-code': 'var(--void-fg-0)',
'--tw-prose-pre-bg': 'var(--void-bg-1)',
'--tw-prose-th-borders': 'var(--void-border-4)',
'--tw-prose-td-borders': 'var(--void-border-4)',
},
},
}),
fontSize: {
xs: '10px',
sm: '11px',
@@ -27,146 +49,150 @@ module.exports = {
// common colors to use, ordered light to dark
colors: {
"void-bg-1": "var(--vscode-input-background)",
"void-bg-1-alt": "var(--vscode-badge-background)",
"void-bg-2": "var(--vscode-sideBar-background)",
"void-bg-2-alt": "color-mix(in srgb, var(--vscode-sideBar-background) 30%, var(--vscode-editor-background) 70%)",
"void-bg-3": "var(--vscode-editor-background)",
'void-bg-1': 'var(--void-bg-1)',
'void-bg-1-alt': 'var(--void-bg-1-alt)',
'void-bg-2': 'var(--void-bg-2)',
'void-bg-2-alt': 'var(--void-bg-2-alt)',
'void-bg-3': 'var(--void-bg-3)',
"void-fg-1": "var(--vscode-editor-foreground)",
"void-fg-2": "var(--vscode-input-foreground)",
"void-fg-3": "var(--vscode-input-placeholderForeground)",
// "void-fg-4": "var(--vscode-tab-inactiveForeground)",
"void-fg-4": "var(--vscode-list-deemphasizedForeground)",
'void-fg-0': 'var(--void-fg-0)',
'void-fg-1': 'var(--void-fg-1)',
'void-fg-2': 'var(--void-fg-2)',
'void-fg-3': 'var(--void-fg-3)',
// 'void-fg-4': 'var(--vscode-tab-inactiveForeground)',
'void-fg-4': 'var(--void-fg-4)',
'void-warning': 'var(--void-warning)',
"void-warning": "var(--vscode-charts-yellow)",
"void-border-1": "var(--vscode-commandCenter-activeBorder)",
"void-border-2": "var(--vscode-commandCenter-border)",
"void-border-3": "var(--vscode-commandCenter-inactiveBorder)",
"void-border-4": "var(--vscode-editorGroup-border)",
'void-border-1': 'var(--void-border-1)',
'void-border-2': 'var(--void-border-2)',
'void-border-3': 'var(--void-border-3)',
'void-border-4': 'var(--void-border-4)',
'void-ring-color': 'var(--void-ring-color)',
'void-link-color': 'var(--void-link-color)',
vscode: {
// see: https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content
// base colors
"fg": "var(--vscode-foreground)",
"focus-border": "var(--vscode-focusBorder)",
"disabled-fg": "var(--vscode-disabledForeground)",
"widget-border": "var(--vscode-widget-border)",
"widget-shadow": "var(--vscode-widget-shadow)",
"selection-bg": "var(--vscode-selection-background)",
"description-fg": "var(--vscode-descriptionForeground)",
"error-fg": "var(--vscode-errorForeground)",
"icon-fg": "var(--vscode-icon-foreground)",
"sash-hover-border": "var(--vscode-sash-hoverBorder)",
'fg': 'var(--vscode-foreground)',
'focus-border': 'var(--vscode-focusBorder)',
'disabled-fg': 'var(--vscode-disabledForeground)',
'widget-border': 'var(--vscode-widget-border)',
'widget-shadow': 'var(--vscode-widget-shadow)',
'selection-bg': 'var(--vscode-selection-background)',
'description-fg': 'var(--vscode-descriptionForeground)',
'error-fg': 'var(--vscode-errorForeground)',
'icon-fg': 'var(--vscode-icon-foreground)',
'sash-hover-border': 'var(--vscode-sash-hoverBorder)',
// text colors
"text-blockquote-bg": "var(--vscode-textBlockQuote-background)",
"text-blockquote-border": "var(--vscode-textBlockQuote-border)",
"text-codeblock-bg": "var(--vscode-textCodeBlock-background)",
"text-link-active-fg": "var(--vscode-textLink-activeForeground)",
"text-link-fg": "var(--vscode-textLink-foreground)",
"text-preformat-fg": "var(--vscode-textPreformat-foreground)",
"text-preformat-bg": "var(--vscode-textPreformat-background)",
"text-separator-fg": "var(--vscode-textSeparator-foreground)",
'text-blockquote-bg': 'var(--vscode-textBlockQuote-background)',
'text-blockquote-border': 'var(--vscode-textBlockQuote-border)',
'text-codeblock-bg': 'var(--vscode-textCodeBlock-background)',
'text-link-active-fg': 'var(--vscode-textLink-activeForeground)',
'text-link-fg': 'var(--vscode-textLink-foreground)',
'text-preformat-fg': 'var(--vscode-textPreformat-foreground)',
'text-preformat-bg': 'var(--vscode-textPreformat-background)',
'text-separator-fg': 'var(--vscode-textSeparator-foreground)',
// input colors
"input-bg": "var(--vscode-input-background)",
"input-border": "var(--vscode-input-border)",
"input-fg": "var(--vscode-input-foreground)",
"input-placeholder-fg": "var(--vscode-input-placeholderForeground)",
"input-active-bg": "var(--vscode-input-activeBackground)",
"input-option-active-border": "var(--vscode-inputOption-activeBorder)",
"input-option-active-fg": "var(--vscode-inputOption-activeForeground)",
"input-option-hover-bg": "var(--vscode-inputOption-hoverBackground)",
"input-validation-error-bg": "var(--vscode-inputValidation-errorBackground)",
"input-validation-error-fg": "var(--vscode-inputValidation-errorForeground)",
"input-validation-error-border": "var(--vscode-inputValidation-errorBorder)",
"input-validation-info-bg": "var(--vscode-inputValidation-infoBackground)",
"input-validation-info-fg": "var(--vscode-inputValidation-infoForeground)",
"input-validation-info-border": "var(--vscode-inputValidation-infoBorder)",
"input-validation-warning-bg": "var(--vscode-inputValidation-warningBackground)",
"input-validation-warning-fg": "var(--vscode-inputValidation-warningForeground)",
"input-validation-warning-border": "var(--vscode-inputValidation-warningBorder)",
'input-bg': 'var(--vscode-input-background)',
'input-border': 'var(--vscode-input-border)',
'input-fg': 'var(--vscode-input-foreground)',
'input-placeholder-fg': 'var(--vscode-input-placeholderForeground)',
'input-active-bg': 'var(--vscode-input-activeBackground)',
'input-option-active-border': 'var(--vscode-inputOption-activeBorder)',
'input-option-active-fg': 'var(--vscode-inputOption-activeForeground)',
'input-option-hover-bg': 'var(--vscode-inputOption-hoverBackground)',
'input-validation-error-bg': 'var(--vscode-inputValidation-errorBackground)',
'input-validation-error-fg': 'var(--vscode-inputValidation-errorForeground)',
'input-validation-error-border': 'var(--vscode-inputValidation-errorBorder)',
'input-validation-info-bg': 'var(--vscode-inputValidation-infoBackground)',
'input-validation-info-fg': 'var(--vscode-inputValidation-infoForeground)',
'input-validation-info-border': 'var(--vscode-inputValidation-infoBorder)',
'input-validation-warning-bg': 'var(--vscode-inputValidation-warningBackground)',
'input-validation-warning-fg': 'var(--vscode-inputValidation-warningForeground)',
'input-validation-warning-border': 'var(--vscode-inputValidation-warningBorder)',
// command center colors (the top bar)
"commandcenter-fg": "var(--vscode-commandCenter-foreground)",
"commandcenter-active-fg": "var(--vscode-commandCenter-activeForeground)",
"commandcenter-bg": "var(--vscode-commandCenter-background)",
"commandcenter-active-bg": "var(--vscode-commandCenter-activeBackground)",
"commandcenter-border": "var(--vscode-commandCenter-border)",
"commandcenter-inactive-fg": "var(--vscode-commandCenter-inactiveForeground)",
"commandcenter-inactive-border": "var(--vscode-commandCenter-inactiveBorder)",
"commandcenter-active-border": "var(--vscode-commandCenter-activeBorder)",
"commandcenter-debugging-bg": "var(--vscode-commandCenter-debuggingBackground)",
'commandcenter-fg': 'var(--vscode-commandCenter-foreground)',
'commandcenter-active-fg': 'var(--vscode-commandCenter-activeForeground)',
'commandcenter-bg': 'var(--vscode-commandCenter-background)',
'commandcenter-active-bg': 'var(--vscode-commandCenter-activeBackground)',
'commandcenter-border': 'var(--vscode-commandCenter-border)',
'commandcenter-inactive-fg': 'var(--vscode-commandCenter-inactiveForeground)',
'commandcenter-inactive-border': 'var(--vscode-commandCenter-inactiveBorder)',
'commandcenter-active-border': 'var(--vscode-commandCenter-activeBorder)',
'commandcenter-debugging-bg': 'var(--vscode-commandCenter-debuggingBackground)',
// badge colors
"badge-fg": "var(--vscode-badge-foreground)",
"badge-bg": "var(--vscode-badge-background)",
'badge-fg': 'var(--vscode-badge-foreground)',
'badge-bg': 'var(--vscode-badge-background)',
// button colors
"button-bg": "var(--vscode-button-background)",
"button-fg": "var(--vscode-button-foreground)",
"button-border": "var(--vscode-button-border)",
"button-separator": "var(--vscode-button-separator)",
"button-hover-bg": "var(--vscode-button-hoverBackground)",
"button-secondary-fg": "var(--vscode-button-secondaryForeground)",
"button-secondary-bg": "var(--vscode-button-secondaryBackground)",
"button-secondary-hover-bg": "var(--vscode-button-secondaryHoverBackground)",
'button-bg': 'var(--vscode-button-background)',
'button-fg': 'var(--vscode-button-foreground)',
'button-border': 'var(--vscode-button-border)',
'button-separator': 'var(--vscode-button-separator)',
'button-hover-bg': 'var(--vscode-button-hoverBackground)',
'button-secondary-fg': 'var(--vscode-button-secondaryForeground)',
'button-secondary-bg': 'var(--vscode-button-secondaryBackground)',
'button-secondary-hover-bg': 'var(--vscode-button-secondaryHoverBackground)',
// checkbox colors
"checkbox-bg": "var(--vscode-checkbox-background)",
"checkbox-fg": "var(--vscode-checkbox-foreground)",
"checkbox-border": "var(--vscode-checkbox-border)",
"checkbox-select-bg": "var(--vscode-checkbox-selectBackground)",
'checkbox-bg': 'var(--vscode-checkbox-background)',
'checkbox-fg': 'var(--vscode-checkbox-foreground)',
'checkbox-border': 'var(--vscode-checkbox-border)',
'checkbox-select-bg': 'var(--vscode-checkbox-selectBackground)',
// sidebar colors
"sidebar-bg": "var(--vscode-sideBar-background)",
"sidebar-fg": "var(--vscode-sideBar-foreground)",
"sidebar-border": "var(--vscode-sideBar-border)",
"sidebar-drop-bg": "var(--vscode-sideBar-dropBackground)",
"sidebar-title-fg": "var(--vscode-sideBarTitle-foreground)",
"sidebar-header-bg": "var(--vscode-sideBarSectionHeader-background)",
"sidebar-header-fg": "var(--vscode-sideBarSectionHeader-foreground)",
"sidebar-header-border": "var(--vscode-sideBarSectionHeader-border)",
"sidebar-activitybartop-border": "var(--vscode-sideBarActivityBarTop-border)",
"sidebar-title-bg": "var(--vscode-sideBarTitle-background)",
"sidebar-title-border": "var(--vscode-sideBarTitle-border)",
"sidebar-stickyscroll-bg": "var(--vscode-sideBarStickyScroll-background)",
"sidebar-stickyscroll-border": "var(--vscode-sideBarStickyScroll-border)",
"sidebar-stickyscroll-shadow": "var(--vscode-sideBarStickyScroll-shadow)",
'sidebar-bg': 'var(--vscode-sideBar-background)',
'sidebar-fg': 'var(--vscode-sideBar-foreground)',
'sidebar-border': 'var(--vscode-sideBar-border)',
'sidebar-drop-bg': 'var(--vscode-sideBar-dropBackground)',
'sidebar-title-fg': 'var(--vscode-sideBarTitle-foreground)',
'sidebar-header-bg': 'var(--vscode-sideBarSectionHeader-background)',
'sidebar-header-fg': 'var(--vscode-sideBarSectionHeader-foreground)',
'sidebar-header-border': 'var(--vscode-sideBarSectionHeader-border)',
'sidebar-activitybartop-border': 'var(--vscode-sideBarActivityBarTop-border)',
'sidebar-title-bg': 'var(--vscode-sideBarTitle-background)',
'sidebar-title-border': 'var(--vscode-sideBarTitle-border)',
'sidebar-stickyscroll-bg': 'var(--vscode-sideBarStickyScroll-background)',
'sidebar-stickyscroll-border': 'var(--vscode-sideBarStickyScroll-border)',
'sidebar-stickyscroll-shadow': 'var(--vscode-sideBarStickyScroll-shadow)',
// other colors (these are partially complete)
// text formatting
"text-preformat-bg": "var(--vscode-textPreformat-background)",
"text-preformat-fg": "var(--vscode-textPreformat-foreground)",
'text-preformat-bg': 'var(--vscode-textPreformat-background)',
'text-preformat-fg': 'var(--vscode-textPreformat-foreground)',
// editor colors
"editor-bg": "var(--vscode-editor-background)",
"editor-fg": "var(--vscode-editor-foreground)",
'editor-bg': 'var(--vscode-editor-background)',
'editor-fg': 'var(--vscode-editor-foreground)',
// other
"editorwidget-bg": "var(--vscode-editorWidget-background)",
"toolbar-hover-bg": "var(--vscode-toolbar-hoverBackground)",
"toolbar-foreground": "var(--vscode-editorActionList-foreground)",
'editorwidget-bg': 'var(--vscode-editorWidget-background)',
'toolbar-hover-bg': 'var(--vscode-toolbar-hoverBackground)',
'toolbar-foreground': 'var(--vscode-editorActionList-foreground)',
"editorwidget-fg": "var(--vscode-editorWidget-foreground)",
"editorwidget-border": "var(--vscode-editorWidget-border)",
'editorwidget-fg': 'var(--vscode-editorWidget-foreground)',
'editorwidget-border': 'var(--vscode-editorWidget-border)',
"charts-orange": "var(--vscode-charts-orange)",
"charts-yellow": "var(--vscode-charts-yellow)",
'charts-orange': 'var(--vscode-charts-orange)',
'charts-yellow': 'var(--vscode-charts-yellow)',
},
},
},
},
plugins: [],
plugins: [
require('@tailwindcss/typography')
],
prefix: 'void-'
}
@@ -7,6 +7,7 @@ import { defineConfig } from 'tsup'
export default defineConfig({
entry: [
'./src2/void-command-bar-tsx/index.tsx',
'./src2/sidebar-tsx/index.tsx',
'./src2/void-settings-tsx/index.tsx',
'./src2/quick-edit-tsx/index.tsx',
@@ -7,8 +7,6 @@ import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// import { ILLMMessageService } from '../common/llmMessageService.js';
// import { ServiceSendLLMMessageParams } from '../common/llmMessageTypes.js';
@@ -17,7 +15,7 @@ export interface ISearchReplaceService {
}
export const ISearchReplaceService = createDecorator<ISearchReplaceService>('SearchReplaceCacheService');
class SearchReplaceService extends Disposable implements ISearchReplaceService {
export class SearchReplaceService extends Disposable implements ISearchReplaceService {
_serviceBrand: undefined;
private readonly _onDidChangeState = new Emitter<void>();
@@ -11,24 +11,21 @@ import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
import { StagingSelectionItem, IChatThreadService } from '../common/chatThreadService.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { localize2 } from '../../../../nls.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
import { IVoidUriStateService } from './voidUriStateService.js';
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
import { IChatThreadService } from './chatThreadService.js';
// ---------- Register commands and keybindings ----------
@@ -122,21 +119,23 @@ registerAction2(class extends Action2 {
const selection: StagingSelectionItem = !selectionRange || !selectionStr || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
fileURI: model.uri,
language: model.getLanguageId(),
selectionStr: null,
range: null,
state: { isOpened: false, }
state: { isOpened: false, wasAddedAsCurrentFile: false }
} : {
type: 'Selection',
fileURI: model.uri,
language: model.getLanguageId(),
selectionStr: selectionStr,
range: selectionRange,
state: { isOpened: true, }
state: { isOpened: true, wasAddedAsCurrentFile: false }
}
// update the staging selections
const chatThreadService = accessor.get(IChatThreadService)
const focusedMessageIdx = chatThreadService.getFocusedMessageIdx()
const focusedMessageIdx = chatThreadService.getCurrentFocusedMessageIdx()
// set the selections to the proper value
let selections: StagingSelectionItem[] = []
@@ -283,43 +282,3 @@ export class TabSwitchListener extends Disposable {
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
}
}
class TabSwitchContribution extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.void.tabswitch'
constructor(
@IInstantiationService private readonly instantiationService: IInstantiationService,
@IViewsService private readonly viewsService: IViewsService,
@IVoidUriStateService private readonly uriStateService: IVoidUriStateService,
@ICodeEditorService private readonly codeEditorService: ICodeEditorService,
// @ICommandService private readonly commandService: ICommandService,
) {
super()
// sidebarIsVisible state
let sidebarIsVisible = this.viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
this._register(this.viewsService.onDidChangeViewVisibility(e => {
sidebarIsVisible = e.visible
}))
const onSwitchTab = () => { // update state
if (sidebarIsVisible) {
const currentUri = this.codeEditorService.getActiveCodeEditor()?.getModel()?.uri
if (!currentUri) return;
this.uriStateService.setState({ currentUri })
// this.commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
}
}
// when sidebar becomes visible, add current file
this._register(this.viewsService.onDidChangeViewVisibility(e => { sidebarIsVisible = e.visible }))
// run on current tab if it exists, and listen for tab switches and visibility changes
onSwitchTab()
this._register(this.viewsService.onDidChangeViewVisibility(() => { onSwitchTab() }))
this._register(this.instantiationService.createInstance(TabSwitchListener, () => { onSwitchTab() }))
}
}
registerWorkbenchContribution2(TabSwitchContribution.ID, TabSwitchContribution, WorkbenchPhase.BlockRestore);
@@ -38,7 +38,7 @@ import { mountSidebar } from './react/out/sidebar-tsx/index.js';
import { Codicon } from '../../../../base/common/codicons.js';
import { Orientation } from '../../../../base/browser/ui/sash/sash.js';
// import { IDisposable } from '../../../../base/common/lifecycle.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { toDisposable } from '../../../../base/common/lifecycle.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
@@ -80,8 +80,8 @@ class SidebarViewPane extends ViewPane {
// gets set immediately
this.instantiationService.invokeFunction(accessor => {
// mount react
const disposables: IDisposable[] | undefined = mountSidebar(parent, accessor);
disposables?.forEach(d => this._register(d))
const disposeFn: (() => void) | undefined = mountSidebar(parent, accessor)?.dispose;
this._register(toDisposable(() => disposeFn?.()))
});
}
@@ -3,69 +3,229 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js';
export interface ITerminalToolService {
readonly _serviceBrand: undefined;
createNewTerminal(terminalId: string): Promise<string>;
runCommand(command: string, terminalId?: string): Promise<void>;
focus(terminalId: string): Promise<void>;
listTerminalIds(): string[];
runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>;
openTerminal(terminalId: string): Promise<void>
terminalExists(terminalId: string): boolean
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
function isCommandComplete(output: string) {
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
if (!completionMatch) { return false }
if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
return { exitCode: 0 }
}
const nameOfId = (id: string) => {
if (id === '1') return 'Void Agent'
return `Void Agent (${id})`
}
const idOfName = (name: string) => {
if (name === 'Void Agent') return '1'
const match = name.match(/Void Agent \((\d+)\)/)
if (!match) return null
if (Number.isInteger(match[1]) && Number(match[1]) >= 1) return match[1]
return null
}
export class TerminalToolService extends Disposable implements ITerminalToolService {
readonly _serviceBrand: undefined;
private terminalInstances: Record<string, ITerminalInstance> = {}
private terminalInstanceOfId: Record<string, ITerminalInstance> = {}
constructor(
@ITerminalService private readonly terminalService: ITerminalService
@ITerminalService private readonly terminalService: ITerminalService,
) {
super();
}
async createNewTerminal() {
const terminalId = generateUuid();
this.terminalService.createTerminal({});
const terminal = await this.terminalService.createTerminal({
location: TerminalLocation.Editor,
config: { name: `Void Agent (${terminalId})`, }
});
this.terminalInstances[terminalId] = terminal
return terminalId;
}
async runCommand(command: string, terminalId?: string) {
if (!terminalId) {
terminalId = await this.createNewTerminal();
// runs on ALL terminals for simplicity
const initializeTerminal = (terminal: ITerminalInstance) => {
// when exit, remove
const d = terminal.onExit(() => {
const terminalId = idOfName(terminal.title)
if (terminalId !== null && (terminalId in this.terminalInstanceOfId)) delete this.terminalInstanceOfId[terminalId]
d.dispose()
})
}
const terminal = this.terminalInstances[terminalId];
if (!terminal) throw new Error(`Terminal with ID ${terminalId} does not exist`);
terminal.sendText(command, true);
return;
// initialize any terminals that are already open
for (const terminal of terminalService.instances) {
const proposedTerminalId = idOfName(terminal.title)
if (proposedTerminalId) this.terminalInstanceOfId[proposedTerminalId] = terminal
initializeTerminal(terminal)
}
this._register(
terminalService.onDidCreateInstance(terminal => { initializeTerminal(terminal) })
)
}
async focus(terminalId: string) {
const terminal = this.terminalInstances[terminalId];
if (!terminal) throw new Error(`That terminal was closed.`);
terminal.focus(true);
return;
listTerminalIds() {
return Object.keys(this.terminalInstanceOfId)
}
getValidNewTerminalId(): string {
// {1 2 3} # size 3, new=4
// {1 3 4} # size 3, new=2
// 1 <= newTerminalId <= n + 1
const n = Object.keys(this.terminalInstanceOfId).length;
if (n === 0) return '1'
for (let i = 1; i <= n + 1; i++) {
const potentialId = i + '';
if (!(potentialId in this.terminalInstanceOfId)) return potentialId;
}
throw new Error('This should never be reached by pigeonhole principle');
}
private async _getOrCreateTerminal(proposedTerminalId: string) {
// if terminal ID exists, return it
if (proposedTerminalId in this.terminalInstanceOfId) return { terminalId: proposedTerminalId, didCreateTerminal: false }
// create new terminal and return its ID
const terminalId = this.getValidNewTerminalId();
const terminal = await this.terminalService.createTerminal({
location: TerminalLocation.Panel,
config: { name: nameOfId(terminalId), title: nameOfId(terminalId) },
})
// when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
const disposables: IDisposable[] = []
const waitForMount = new Promise<void>(res => {
let data = ''
const d = terminal.onData(newData => {
data += newData
if (isCommandComplete(data)) { res() }
})
disposables.push(d)
})
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 1000) })
await Promise.any([waitForMount, waitForTimeout,])
disposables.forEach(d => d.dispose())
this.terminalInstanceOfId[terminalId] = terminal
return { terminalId, didCreateTerminal: true }
}
terminalExists(terminalId: string): boolean {
return terminalId in this.terminalInstanceOfId
}
openTerminal: ITerminalToolService['openTerminal'] = async (terminalId) => {
if (!terminalId) return
const terminal = this.terminalInstanceOfId[terminalId]
if (!terminal) return // should never happen
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
}
runCommand: ITerminalToolService['runCommand'] = async (command, proposedTerminalId, waitForCompletion) => {
await this.terminalService.whenConnected;
const { terminalId, didCreateTerminal } = await this._getOrCreateTerminal(proposedTerminalId)
const terminal = this.terminalInstanceOfId[terminalId];
if (!terminal) throw new Error(`Unexpected internal error: Terminal with ID ${terminalId} did not exist.`);
// focus the terminal about to run
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
const disposables: IDisposable[] = []
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
result += newData
// onPageFull
if (result.length > MAX_TERMINAL_CHARS_PAGE) {
result = result.substring(0, MAX_TERMINAL_CHARS_PAGE)
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
resolveReason = { type: 'toofull' }
res()
return
}
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
})
disposables.push(d2)
})
// send the command here
await terminal.sendText(command, true)
// timeout promise
const waitUntilTimeout = new Promise<void>((res, rej) => {
setTimeout(async () => {
if (resolveReason) return
await terminal.sendText('\x03', true) // interrupt the terminal with Ctrl+C
resolveReason = { type: waitForCompletion ? 'timeout' : 'bgtask' }
res()
return
}, (waitForCompletion ? TERMINAL_TIMEOUT_TIME : TERMINAL_BG_WAIT_TIME) * 1000)
})
await Promise.any([
waitUntilDone,
waitUntilTimeout,
])
disposables.forEach(d => d.dispose())
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
result = removeAnsiEscapeCodes(result)
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
return { terminalId, didCreateTerminal, result, resolveReason }
}
}
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Eager);
registerSingleton(ITerminalToolService, TerminalToolService, InstantiationType.Delayed);
@@ -0,0 +1,440 @@
import { CancellationToken } from '../../../../base/common/cancellation.js'
import { URI } from '../../../../base/common/uri.js'
import { IFileService } from '../../../../platform/files/common/files.js'
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js'
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'
import { QueryBuilder } from '../../../services/search/common/queryBuilder.js'
import { ISearchService } from '../../../services/search/common/search.js'
import { IEditCodeService } from './editCodeServiceInterface.js'
import { ITerminalToolService } from './terminalToolService.js'
import { ToolCallParams, ToolDirectoryItem, ToolName, ToolResultType } from '../common/toolsServiceTypes.js'
import { IVoidModelService } from '../common/voidModelService.js'
import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { basename } from '../../../../base/common/path.js'
import { IVoidCommandBarService } from './voidCommandBarService.js'
// tool use for AI
type ValidateParams = { [T in ToolName]: (p: string) => Promise<ToolCallParams[T]> }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string }
// pagination info
const MAX_FILE_CHARS_PAGE = 50_000
const MAX_CHILDREN_URIs_PAGE = 500
export const MAX_TERMINAL_CHARS_PAGE = 20_000
export const TERMINAL_TIMEOUT_TIME = 15
export const TERMINAL_BG_WAIT_TIME = 1
const computeDirectoryResult = async (
fileService: IFileService,
rootURI: URI,
pageNumber: number = 1
): Promise<ToolResultType['list_dir']> => {
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
if (!stat.isDirectory) {
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
}
const originalChildrenLength = stat.children?.length ?? 0;
const fromChildIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1);
const toChildIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1; // INCLUSIVE
const listChildren = stat.children?.slice(fromChildIdx, toChildIdx + 1) ?? [];
const children: ToolDirectoryItem[] = listChildren.map(child => ({
name: child.name,
uri: child.resource,
isDirectory: child.isDirectory,
isSymbolicLink: child.isSymbolicLink
}));
const hasNextPage = (originalChildrenLength - 1) > toChildIdx;
const hasPrevPage = pageNumber > 1;
const itemsRemaining = Math.max(0, originalChildrenLength - (toChildIdx + 1));
return {
children,
hasNextPage,
hasPrevPage,
itemsRemaining
};
};
const directoryResultToString = (params: ToolCallParams['list_dir'], result: ToolResultType['list_dir']): string => {
if (!result.children) {
return `Error: ${params.rootURI} is not a directory`;
}
let output = '';
const entries = result.children;
if (!result.hasPrevPage) { // is first page
output += `${params.rootURI.fsPath}\n`;
}
for (let i = 0; i < entries.length; i++) {
const entry = entries[i];
const isLast = i === entries.length - 1 && !result.hasNextPage;
const prefix = isLast ? '└── ' : '├── ';
output += `${prefix}${entry.name}${entry.isDirectory ? '/' : ''}${entry.isSymbolicLink ? ' (symbolic link)' : ''}\n`;
}
if (result.hasNextPage) {
output += `└── (${result.itemsRemaining} results remaining...)\n`;
}
return output;
};
const validateJSON = (s: string): { [s: string]: unknown } => {
try {
const o = JSON.parse(s)
if (typeof o !== 'object') throw new Error()
if ('result' in o) { // openrouter sometimes wraps the result with { 'result': ... }
return o.result
}
return o
}
catch (e) {
throw new Error(`Invalid LLM output format: Tool parameter was not a string of a valid JSON: "${s}".`)
}
}
const validateStr = (argName: string, value: unknown) => {
if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string.`)
return value
}
// We are NOT checking to make sure in workspace
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error('Invalid LLM output format: Provided uri must be a string.')
const uri = URI.file(uriStr)
return uri
}
const validatePageNum = (pageNumberUnknown: unknown) => {
if (!pageNumberUnknown) return 1
const parsedInt = Number.parseInt(pageNumberUnknown + '')
if (!Number.isInteger(parsedInt)) throw new Error(`Page number was not an integer: "${pageNumberUnknown}".`)
if (parsedInt < 1) throw new Error(`Invalid LLM output format: Specified page number must be 1 or greater: "${pageNumberUnknown}".`)
return parsedInt
}
const validateRecursiveParamStr = (paramsUnknown: unknown) => {
if (typeof paramsUnknown !== 'string') throw new Error('Invalid LLM output format: Error calling tool: provided params must be a string.')
const params = paramsUnknown
const isRecursive = params.includes('r')
return isRecursive
}
const validateProposedTerminalId = (terminalIdUnknown: unknown) => {
if (!terminalIdUnknown) return '1'
const terminalId = terminalIdUnknown + ''
return terminalId
}
const validateWaitForCompletion = (b: unknown) => {
if (typeof b === 'string') {
if (b === 'true') return true
if (b === 'false') return false
}
return true // default is true
}
const checkIfIsFolder = (uriStr: string) => {
uriStr = uriStr.trim()
if (uriStr.endsWith('/') || uriStr.endsWith('\\')) return true
return false
}
export interface IToolsService {
readonly _serviceBrand: undefined;
validateParams: ValidateParams;
callTool: CallTool;
stringOfResult: ToolResultToString;
}
export const IToolsService = createDecorator<IToolsService>('ToolsService');
export class ToolsService implements IToolsService {
readonly _serviceBrand: undefined;
public validateParams: ValidateParams;
public callTool: CallTool;
public stringOfResult: ToolResultToString;
constructor(
@IFileService fileService: IFileService,
@IWorkspaceContextService workspaceContextService: IWorkspaceContextService,
@ISearchService searchService: ISearchService,
@IInstantiationService instantiationService: IInstantiationService,
@IVoidModelService voidModelService: IVoidModelService,
@IEditCodeService editCodeService: IEditCodeService,
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
@IVoidCommandBarService private readonly commandBarService: IVoidCommandBarService,
) {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.validateParams = {
read_file: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { uri, pageNumber }
},
list_dir: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, pageNumber: pageNumberUnknown } = o
const uri = validateURI(uriStr)
const pageNumber = validatePageNum(pageNumberUnknown)
return { rootURI: uri, pageNumber }
},
pathname_search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
return { queryStr, pageNumber }
},
grep_search: async (params: string) => {
const o = validateJSON(params)
const { query: queryUnknown, pageNumber: pageNumberUnknown } = o
const queryStr = validateStr('query', queryUnknown)
const pageNumber = validatePageNum(pageNumberUnknown)
return { queryStr, pageNumber }
},
// ---
create_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriUnknown } = o
const uri = validateURI(uriUnknown)
const uriStr = validateStr('uri', uriUnknown)
const isFolder = checkIfIsFolder(uriStr)
return { uri, isFolder }
},
delete_uri: async (params: string) => {
const o = validateJSON(params)
const { uri: uriUnknown, params: paramsStr } = o
const uri = validateURI(uriUnknown)
const isRecursive = validateRecursiveParamStr(paramsStr)
const uriStr = validateStr('uri', uriUnknown)
const isFolder = checkIfIsFolder(uriStr)
return { uri, isRecursive, isFolder }
},
edit: async (params: string) => {
const o = validateJSON(params)
const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o
const uri = validateURI(uriStr)
const changeDescription = validateStr('changeDescription', changeDescriptionUnknown)
return { uri, changeDescription }
},
terminal_command: async (s: string) => {
const o = validateJSON(s)
const { command: commandUnknown, terminalId: terminalIdUnknown, waitForCompletion: waitForCompletionUnknown } = o
const command = validateStr('command', commandUnknown)
const proposedTerminalId = validateProposedTerminalId(terminalIdUnknown)
const waitForCompletion = validateWaitForCompletion(waitForCompletionUnknown)
return { command, proposedTerminalId, waitForCompletion }
},
}
this.callTool = {
read_file: async ({ uri, pageNumber }) => {
await voidModelService.initializeModel(uri)
const { model } = await voidModelService.getModelSafe(uri)
if (model === null) { throw new Error(`Contents were empty. There may have been an error, or the file may not exist.`) }
const readFileContents = model.getValue(EndOfLinePreference.LF)
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (readFileContents.length - 1) - toIdx >= 1
return { result: { fileContents, hasNextPage } }
},
list_dir: async ({ rootURI, pageNumber }) => {
const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber)
return { result: dirResult }
},
pathname_search: async ({ queryStr, pageNumber }) => {
const query = queryBuilder.file(workspaceContextService.getWorkspace().folders.map(f => f.uri), {
filePattern: queryStr,
})
const data = await searchService.fileSearch(query, CancellationToken.None)
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
const uris = data.results
.slice(fromIdx, toIdx + 1) // paginate
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
return { result: { uris, hasNextPage } }
},
grep_search: async ({ queryStr, pageNumber }) => {
const query = queryBuilder.text({
pattern: queryStr,
isRegExp: true,
}, workspaceContextService.getWorkspace().folders.map(f => f.uri))
const data = await searchService.textSearch(query, CancellationToken.None)
const fromIdx = MAX_CHILDREN_URIs_PAGE * (pageNumber - 1)
const toIdx = MAX_CHILDREN_URIs_PAGE * pageNumber - 1
const uris = data.results
.slice(fromIdx, toIdx + 1) // paginate
.map(({ resource, results }) => resource)
const hasNextPage = (data.results.length - 1) - toIdx >= 1
return { result: { queryStr, uris, hasNextPage } }
},
// ---
create_uri: async ({ uri, isFolder }) => {
if (isFolder)
await fileService.createFolder(uri)
else {
await fileService.createFile(uri)
}
return { result: {} }
},
delete_uri: async ({ uri, isRecursive }) => {
await fileService.del(uri, { recursive: isRecursive })
return { result: {} }
},
edit: async ({ uri, changeDescription }) => {
await voidModelService.initializeModel(uri)
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`The Apply model was already running. This can happen if two agents try editing the same file at the same time. Please try again in a moment.`)
}
const res = await editCodeService.startApplying({
uri,
applyStr: changeDescription,
from: 'ClickApply',
startBehavior: 'keep-conflicts',
})
if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`)
const [diffZoneURI, applyDonePromise] = res
const interruptTool = () => { // must reject the applyPromiseDone promise
editCodeService.interruptURIStreaming({ uri: diffZoneURI })
}
return { result: applyDonePromise, interruptTool }
},
terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => {
const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion)
return { result: { terminalId, didCreateTerminal, result, resolveReason } }
},
}
const nextPageStr = (hasNextPage: boolean) => hasNextPage ? '\n\n(more on next page...)' : ''
// given to the LLM after the call
this.stringOfResult = {
read_file: (params, result) => {
return result.fileContents + nextPageStr(result.hasNextPage)
},
list_dir: (params, result) => {
const dirTreeStr = directoryResultToString(params, result)
return dirTreeStr // + nextPageStr(result.hasNextPage) // already handles num results remaining
},
pathname_search: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
grep_search: (params, result) => {
return result.uris.map(uri => uri.fsPath).join('\n') + nextPageStr(result.hasNextPage)
},
// ---
create_uri: (params, result) => {
return `URI ${params.uri.fsPath} successfully created.`
},
delete_uri: (params, result) => {
return `URI ${params.uri.fsPath} successfully deleted.`
},
edit: (params, result) => {
console.log('STR OF RESULT', params)
return `Change successfully made to ${params.uri.fsPath}.`
},
terminal_command: (params, result) => {
const {
terminalId,
didCreateTerminal,
resolveReason,
result: result_,
} = result
const terminalDesc = `terminal ${terminalId}${didCreateTerminal ? ` (a newly-created terminal)` : ''}`
if (resolveReason.type === 'timeout') {
return `Terminal command ran in ${terminalDesc}, but timed out after ${TERMINAL_TIMEOUT_TIME} seconds. Result:\n${result_}`
}
else if (resolveReason.type === 'bgtask') {
return `Terminal command is running in the background in ${terminalDesc}. Here were the outputs after ${TERMINAL_BG_WAIT_TIME} seconds:\n${result_}`
}
else if (resolveReason.type === 'toofull') {
return `Terminal command executed in terminal ${terminalDesc}. Command was interrupted because output was too long. Result:\n${result_}`
}
else if (resolveReason.type === 'done') {
return `Terminal command executed in terminal ${terminalDesc}. Result (exit code ${resolveReason.exitCode}):\n${result_}`
}
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
}
}
}
registerSingleton(IToolsService, ToolsService, InstantiationType.Eager);
@@ -33,12 +33,23 @@ import './media/void.css'
import './voidUpdateActions.js'
// tools
import './toolsService.js'
import './terminalToolService.js'
// register Thread History
import './chatThreadService.js'
// ping
import './metricsPollService.js'
// helper services
import './helperServices/consistentItemService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
import '../common/llmMessageService.js'
import '../common/sendLLMMessageService.js'
// voidSettings
import '../common/voidSettingsService.js'
@@ -52,9 +63,5 @@ import '../common/metricsService.js'
// updates
import '../common/voidUpdateService.js'
// tools
import '../common/toolsService.js'
// register Thread History
import '../common/chatThreadService.js'
// model service
import '../common/voidModelService.js'
@@ -0,0 +1,443 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { createDecorator, IInstantiationService } from '../../../../platform/instantiation/common/instantiation.js';
import { URI } from '../../../../base/common/uri.js';
import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { IOverlayWidget, ICodeEditor, OverlayWidgetPositionPreference } from '../../../../editor/browser/editorBrowser.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { mountVoidCommandBar } from './react/out/void-command-bar-tsx/index.js'
import { deepClone } from '../../../../base/common/objects.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { IEditCodeService } from './editCodeServiceInterface.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { generateUuid } from '../../../../base/common/uuid.js';
export interface IVoidCommandBarService {
readonly _serviceBrand: undefined;
stateOfURI: { [uri: string]: CommandBarStateType };
sortedURIs: URI[];
activeURI: URI | null;
onDidChangeState: Event<{ uri: URI }>;
onDidChangeActiveURI: Event<{ uri: URI | null }>;
getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes';
setDiffIdx(uri: URI, newIdx: number | null): void;
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void;
anyFileIsStreaming(): boolean;
}
export const IVoidCommandBarService = createDecorator<IVoidCommandBarService>('VoidCommandBarService');
export type CommandBarStateType = undefined | {
sortedDiffZoneIds: string[]; // sorted by line number
sortedDiffIds: string[]; // sorted by line number (computed)
isStreaming: boolean; // is any diffZone streaming in this URI
diffIdx: number | null; // must refresh whenever sortedDiffIds does so it's valid
}
const defaultState: NonNullable<CommandBarStateType> = {
sortedDiffZoneIds: [],
sortedDiffIds: [],
isStreaming: false,
diffIdx: null,
}
export class VoidCommandBarService extends Disposable implements IVoidCommandBarService {
_serviceBrand: undefined;
static readonly ID: 'void.VoidCommandBarService'
// depends on uri -> diffZone -> {streaming, diffs}
public stateOfURI: { [uri: string]: CommandBarStateType } = {}
public sortedURIs: URI[] = [] // keys of state (depends on diffZones in the uri)
private readonly _listenToTheseURIs = new Set<URI>() // uriFsPaths
// Emits when a URI's stream state changes between idle, streaming, and acceptRejectAll
private readonly _onDidChangeState = new Emitter<{ uri: URI }>();
readonly onDidChangeState = this._onDidChangeState.event;
// active URI
activeURI: URI | null = null;
private readonly _onDidChangeActiveURI = new Emitter<{ uri: URI | null }>();
readonly onDidChangeActiveURI = this._onDidChangeActiveURI.event;
constructor(
@IInstantiationService private readonly _instantiationService: IInstantiationService,
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IModelService private readonly _modelService: IModelService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
) {
super();
const registeredModelURIs = new Set<string>()
const initializeModel = async (model: ITextModel) => {
// do not add listeners to the same model twice - important, or will see duplicates
if (registeredModelURIs.has(model.uri.fsPath)) return
registeredModelURIs.add(model.uri.fsPath)
this._listenToTheseURIs.add(model.uri)
}
// initialize all existing models + initialize when a new model mounts
this._modelService.getModels().forEach(model => { initializeModel(model) })
this._register(this._modelService.onModelAdded(model => { initializeModel(model) }));
// for every new editor, add the floating widget and update active URI
const disposablesOfEditorId: { [editorId: string]: IDisposable[] } = {};
const onCodeEditorAdd = (editor: ICodeEditor) => {
const id = editor.getId();
disposablesOfEditorId[id] = [];
// mount the command bar
const d1 = this._instantiationService.createInstance(AcceptRejectAllFloatingWidget, { editor });
disposablesOfEditorId[id].push(d1);
const d2 = editor.onDidChangeModel((e) => {
if (e.newModelUrl?.scheme !== 'file') return
this.activeURI = e.newModelUrl;
this._onDidChangeActiveURI.fire({ uri: e.newModelUrl })
})
disposablesOfEditorId[id].push(d2);
}
const onCodeEditorRemove = (editor: ICodeEditor) => {
const id = editor.getId();
if (disposablesOfEditorId[id]) {
disposablesOfEditorId[id].forEach(d => d.dispose());
delete disposablesOfEditorId[id];
}
}
this._register(this._codeEditorService.onCodeEditorAdd((editor) => { onCodeEditorAdd(editor) }))
this._register(this._codeEditorService.onCodeEditorRemove((editor) => { onCodeEditorRemove(editor) }))
this._codeEditorService.listCodeEditors().forEach(editor => { onCodeEditorAdd(editor) })
// state updaters
this._register(this._editCodeService.onDidAddOrDeleteDiffZones(e => {
for (const uri of this._listenToTheseURIs) {
if (e.uri.fsPath !== uri.fsPath) continue
// --- sortedURIs: delete if empty, add if not in state yet
const diffZones = this._getDiffZonesOnURI(uri)
if (diffZones.length === 0) {
this._deleteURIEntryFromState(uri)
this._onDidChangeState.fire({ uri })
continue // deleted, so done
}
if (!this.sortedURIs.find(uri2 => uri2.fsPath === uri.fsPath)) {
this._addURIEntryToState(uri)
}
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
// update state of the diffZones on this URI
const oldDiffZones = currState.sortedDiffZoneIds
const currentDiffZones = this._editCodeService.diffAreasOfURI[uri.fsPath] || [] // a Set
const { addedDiffZones, deletedDiffZones } = this._getDiffZoneChanges(oldDiffZones, currentDiffZones || [])
const diffZonesWithoutDeleted = oldDiffZones.filter(olddiffareaid => !deletedDiffZones.has(olddiffareaid))
// --- new state:
const newSortedDiffZoneIds = [
...diffZonesWithoutDeleted,
...addedDiffZones,
]
const newSortedDiffIds = this._computeSortedDiffs(newSortedDiffZoneIds)
const isStreaming = this._isAnyDiffZoneStreaming(currentDiffZones)
this._setState(uri, {
sortedDiffZoneIds: newSortedDiffZoneIds,
sortedDiffIds: newSortedDiffIds,
isStreaming: isStreaming
})
this._onDidChangeState.fire({ uri })
}
}))
this._register(this._editCodeService.onDidChangeDiffsInDiffZone(e => {
for (const uri of this._listenToTheseURIs) {
if (e.uri.fsPath !== uri.fsPath) continue
// --- sortedURIs: no change
// --- state:
// sortedDiffIds gets a change to it, so gets recomputed
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
const { sortedDiffZoneIds } = currState
const newSortedDiffIds = this._computeSortedDiffs(sortedDiffZoneIds)
this._setState(uri, {
sortedDiffIds: newSortedDiffIds,
// sortedDiffZoneIds, // no change
// isStreaming, // no change
})
this._onDidChangeState.fire({ uri })
}
}))
this._register(this._editCodeService.onDidChangeStreamingInDiffZone(e => {
for (const uri of this._listenToTheseURIs) {
if (e.uri.fsPath !== uri.fsPath) continue
// --- sortedURIs: no change
// --- state:
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
const { sortedDiffZoneIds } = currState
this._setState(uri, {
isStreaming: this._isAnyDiffZoneStreaming(sortedDiffZoneIds),
// sortedDiffIds, // no change
// sortedDiffZoneIds, // no change
})
this._onDidChangeState.fire({ uri })
}
}))
}
setDiffIdx(uri: URI, newIdx: number | null): void {
this._setState(uri, { diffIdx: newIdx });
this._onDidChangeState.fire({ uri });
}
getStreamState(uri: URI) {
const { isStreaming, sortedDiffZoneIds } = this.stateOfURI[uri.fsPath] ?? {}
if (isStreaming) {
return 'streaming'
}
if ((sortedDiffZoneIds?.length ?? 0) > 0) {
return 'idle-has-changes'
}
return 'idle-no-changes'
}
_computeSortedDiffs(diffareaids: string[]) {
const sortedDiffIds = [];
for (const diffareaid of diffareaids) {
const diffZone = this._editCodeService.diffAreaOfId[diffareaid];
if (!diffZone || diffZone.type !== 'DiffZone') {
continue;
}
// Add all diff ids from this diffzone
const diffIds = Object.keys(diffZone._diffOfId);
sortedDiffIds.push(...diffIds);
}
return sortedDiffIds;
}
_getDiffZoneChanges(oldDiffZones: Iterable<string>, currentDiffZones: Iterable<string>) {
// Find the added or deleted diffZones by comparing diffareaids
const addedDiffZoneIds = new Set<string>();
const deletedDiffZoneIds = new Set<string>();
// Convert the current diffZones to a set of ids for easy lookup
const currentDiffZoneIdSet = new Set(currentDiffZones);
// Find deleted diffZones (in old but not in current)
for (const oldDiffZoneId of oldDiffZones) {
if (!currentDiffZoneIdSet.has(oldDiffZoneId)) {
const diffZone = this._editCodeService.diffAreaOfId[oldDiffZoneId];
if (diffZone && diffZone.type === 'DiffZone') {
deletedDiffZoneIds.add(oldDiffZoneId);
}
}
}
// Find added diffZones (in current but not in old)
const oldDiffZoneIdSet = new Set(oldDiffZones);
for (const currentDiffZoneId of currentDiffZones) {
if (!oldDiffZoneIdSet.has(currentDiffZoneId)) {
const diffZone = this._editCodeService.diffAreaOfId[currentDiffZoneId];
if (diffZone && diffZone.type === 'DiffZone') {
addedDiffZoneIds.add(currentDiffZoneId);
}
}
}
return { addedDiffZones: addedDiffZoneIds, deletedDiffZones: deletedDiffZoneIds }
}
_isAnyDiffZoneStreaming(diffareaids: Iterable<string>) {
for (const diffareaid of diffareaids) {
const diffZone = this._editCodeService.diffAreaOfId[diffareaid];
if (!diffZone || diffZone.type !== 'DiffZone') {
continue;
}
if (diffZone._streamState.isStreaming) {
return true;
}
}
return false
}
_setState(uri: URI, opts: Partial<CommandBarStateType>) {
const newState = {
...this.stateOfURI[uri.fsPath] ?? deepClone(defaultState),
...opts
}
// make sure diffIdx is always correct
if (newState.diffIdx && newState.diffIdx > newState.sortedDiffIds.length) {
newState.diffIdx = newState.sortedDiffIds.length
if (newState.diffIdx < 0) newState.diffIdx = null
}
this.stateOfURI = {
...this.stateOfURI,
[uri.fsPath]: newState
}
}
_addURIEntryToState(uri: URI) {
// add to sortedURIs
this.sortedURIs = [
...this.sortedURIs,
uri
]
// add to state
this.stateOfURI[uri.fsPath] = deepClone(defaultState)
}
_deleteURIEntryFromState(uri: URI) {
// delete this from sortedURIs
const i = this.sortedURIs.findIndex(uri2 => uri2.fsPath === uri.fsPath)
if (i === -1) return
this.sortedURIs = [
...this.sortedURIs.slice(0, i),
...this.sortedURIs.slice(i + 1, Infinity),
]
// delete from state
delete this.stateOfURI[uri.fsPath]
}
private _getDiffZonesOnURI(uri: URI) {
const diffZones = [...this._editCodeService.diffAreasOfURI[uri.fsPath]?.values() ?? []]
.map(diffareaid => this._editCodeService.diffAreaOfId[diffareaid])
.filter(diffArea => !!diffArea && diffArea.type === 'DiffZone')
return diffZones
}
anyFileIsStreaming() {
return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming')
}
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) {
const { behavior } = opts
// if anything is streaming, do nothing
const anyIsStreaming = this.anyFileIsStreaming()
if (anyIsStreaming) return
for (const uri of this.sortedURIs) {
this._editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior, removeCtrlKs: false })
}
}
}
registerSingleton(IVoidCommandBarService, VoidCommandBarService, InstantiationType.Delayed); // delayed is needed here :(
// registerWorkbenchContribution2(VoidCommandBarService.ID, VoidCommandBarService, WorkbenchPhase.BlockRestore);
export type VoidCommandBarProps = {
uri: URI | null;
editor: ICodeEditor;
}
class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
private readonly _domNode: HTMLElement;
private readonly editor: ICodeEditor;
private readonly ID: string;
_height = 0
constructor({ editor }: { editor: ICodeEditor, },
@IInstantiationService private readonly instantiationService: IInstantiationService,
) {
super();
this.ID = generateUuid();
this.editor = editor;
// Create container div
const { root } = dom.h('div@root');
// Style the container
// root.style.backgroundColor = 'rgb(248 113 113)';
root.style.height = '16rem'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
root.style.width = '16rem';
root.style.flexDirection = 'column';
root.style.justifyContent = 'flex-end';
root.style.alignItems = 'flex-end';
root.style.zIndex = '2';
root.style.padding = '4px';
root.style.pointerEvents = 'none';
root.style.display = 'flex';
root.style.overflow = 'hidden';
this._domNode = root;
editor.addOverlayWidget(this);
this.instantiationService.invokeFunction(accessor => {
const uri = editor.getModel()?.uri || null
const res = mountVoidCommandBar(root, accessor, { uri, editor } satisfies VoidCommandBarProps)
if (!res) return
this._register(toDisposable(() => res.dispose?.()))
this._register(editor.onWillChangeModel((model) => {
const uri = model.newModelUrl
res.rerender({ uri, editor } satisfies VoidCommandBarProps)
}))
})
}
public getId(): string {
return this.ID;
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition() {
return {
preference: OverlayWidgetPositionPreference.BOTTOM_RIGHT_CORNER
}
}
public override dispose(): void {
this.editor.removeOverlayWidget(this);
super.dispose();
}
}
@@ -25,7 +25,7 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { mountVoidSettings } from './react/out/void-settings-tsx/index.js'
import { Codicon } from '../../../../base/common/codicons.js';
import { IDisposable } from '../../../../base/common/lifecycle.js';
import { toDisposable } from '../../../../base/common/lifecycle.js';
// refer to preferences.contribution.ts keybindings editor
@@ -90,12 +90,12 @@ class VoidSettingsPane extends EditorPane {
// Mount React into the scrollable content
this.instantiationService.invokeFunction(accessor => {
const disposables: IDisposable[] | undefined = mountVoidSettings(settingsElt, accessor);
const disposeFn = mountVoidSettings(settingsElt, accessor)?.dispose;
this._register(toDisposable(() => disposeFn?.()))
// setTimeout(() => { // this is a complete hack and I don't really understand how scrollbar works here
// this._scrollbar?.scanDomNode();
// }, 1000)
disposables?.forEach(d => this._register(d));
});
}
@@ -17,11 +17,41 @@ import * as dom from '../../../../base/browser/dom.js';
const notifyYesUpdate = (notifService: INotificationService, msg?: string) => {
const message = msg || 'This is a very old version of void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
notifService.notify({
const notifyYesUpdate = (notifService: INotificationService, res: { message?: string } = {}) => {
const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
const notifController = notifService.notify({
severity: Severity.Info,
message: message,
sticky: true,
progress: { worked: 0, total: 100 },
actions: {
primary: [{
id: 'void.updater.update',
enabled: true,
label: `Reinstall`,
tooltip: '',
class: undefined,
run: () => {
const { window } = dom.getActiveWindow()
window.open('https://voideditor.com/download-beta')
}
},
{
id: 'void.updater.site',
enabled: true,
label: `Void Site`,
tooltip: '',
class: undefined,
run: () => {
const { window } = dom.getActiveWindow()
window.open('https://voideditor.com/')
}
}]
},
})
const d = notifController.onDidClose(() => {
notifyYesUpdate(notifService, res)
d.dispose()
})
}
const notifyNoUpdate = (notifService: INotificationService) => {
@@ -35,6 +65,7 @@ const notifyErrChecking = (notifService: INotificationService) => {
notifService.notify({
severity: Severity.Info,
message: message,
sticky: true,
})
}
@@ -57,7 +88,7 @@ registerAction2(class extends Action2 {
metricsService.capture('Void Update Manual: Checking...', {})
const res = await voidUpdateService.check()
if (!res) { notifyErrChecking(notifService); metricsService.capture('Void Update Manual: Error', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(notifService, res.message); metricsService.capture('Void Update Manual: Yes', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(notifService, res); metricsService.capture('Void Update Manual: Yes', { res }) }
else if (!res.hasUpdate) { notifyNoUpdate(notifService); metricsService.capture('Void Update Manual: No', { res }) }
}
})
@@ -75,19 +106,19 @@ class VoidUpdateWorkbenchContribution extends Disposable implements IWorkbenchCo
this.metricsService.capture('Void Update Startup: Checking...', {})
const res = await this.voidUpdateService.check()
if (!res) { notifyErrChecking(this.notifService); this.metricsService.capture('Void Update Startup: Error', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res.message); this.metricsService.capture('Void Update Startup: Yes', { res }) }
else if (res.hasUpdate) { notifyYesUpdate(this.notifService, res); this.metricsService.capture('Void Update Startup: Yes', { res }) }
else if (!res.hasUpdate) { this.metricsService.capture('Void Update Startup: No', { res }) } // display nothing if up to date
}
// check once 5 seconds after mount
const initId = setTimeout(() => autoCheck(), 5 * 1000)
this._register({ dispose: () => clearTimeout(initId) })
// check every 3 hours
const { window } = dom.getActiveWindow()
const intervalId = window.setInterval(() => autoCheck(), 3 * 60 * 60 * 1000)
const initId = window.setTimeout(() => autoCheck(), 5 * 1000)
this._register({ dispose: () => window.clearTimeout(initId) })
const intervalId = window.setInterval(() => autoCheck(), 3 * 60 * 60 * 1000) // every 3 hrs
this._register({ dispose: () => window.clearInterval(intervalId) })
}
@@ -1,57 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { URI } from '../../../../base/common/uri.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
// service that manages state
export type VoidUriState = {
currentUri?: URI
}
export interface IVoidUriStateService {
readonly _serviceBrand: undefined;
readonly state: VoidUriState; // readonly to the user
setState(newState: Partial<VoidUriState>): void;
onDidChangeState: Event<void>;
}
export const IVoidUriStateService = createDecorator<IVoidUriStateService>('voidUriStateService');
class VoidUriStateService extends Disposable implements IVoidUriStateService {
_serviceBrand: undefined;
static readonly ID = 'voidUriStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
// state
state: VoidUriState
constructor(
) {
super()
// initial state
this.state = { currentUri: undefined }
}
setState(newState: Partial<VoidUriState>) {
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
}
registerSingleton(IVoidUriStateService, VoidUriStateService, InstantiationType.Eager);
@@ -1,631 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { ILLMMessageService } from './llmMessageService.js';
import { chat_userMessageContent, chat_systemMessage, chat_userMessageContentWithAllFilesToo, chat_selectionsString } from '../browser/prompt/prompts.js';
import { InternalToolInfo, IToolsService, ToolCallReturnType, ToolName, voidTools } from './toolsService.js';
import { toLLMChatMessage, ToolCallType } from './llmMessageTypes.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IVoidFileService } from './voidFileService.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { getErrorMessage } from '../../../../base/common/errors.js';
const findLastIndex = <T>(arr: T[], condition: (t: T) => boolean): number => {
for (let i = arr.length - 1; i >= 0; i--) {
if (condition(arr[i])) {
return i;
}
}
return -1;
}
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
export type CodeSelection = {
type: 'Selection';
fileURI: URI;
selectionStr: string;
range: IRange;
state: {
isOpened: boolean;
};
}
export type FileSelection = {
type: 'File';
fileURI: URI;
selectionStr: null;
range: null;
state: {
isOpened: boolean;
};
}
export type StagingSelectionItem = CodeSelection | FileSelection
export type ToolMessage<T extends ToolName> = {
role: 'tool';
name: T; // internal use
params: string; // internal use
id: string; // apis require this tool use id
content: string; // give this result to LLM
result: { type: 'success'; value: ToolCallReturnType[T], } | { type: 'error'; value: string }; // give this result to user
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
{
role: 'user';
content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
displayContent: string | null; // content displayed to user - allowed to be '', will be ignored
selections: StagingSelectionItem[] | null; // the user's selection
state: {
stagingSelections: StagingSelectionItem[];
isBeingEdited: boolean;
}
} | {
role: 'assistant';
content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking
}
| ToolMessage<ToolName>
type UserMessageType = ChatMessage & { role: 'user' }
type UserMessageState = UserMessageType['state']
export const defaultMessageState: UserMessageState = {
stagingSelections: [],
isBeingEdited: false,
}
// a 'thread' means a chat message history
export type ChatThreads = {
[id: string]: {
id: string; // store the id here too
createdAt: string; // ISO string
lastModified: string; // ISO string
messages: ChatMessage[];
state: {
stagingSelections: StagingSelectionItem[];
focusedMessageIdx: number | undefined; // index of the message that is being edited (undefined if none)
isCheckedOfSelectionId: { [selectionId: string]: boolean }; // TODO
}
};
}
type ThreadType = ChatThreads[string]
const defaultThreadState: ThreadType['state'] = {
stagingSelections: [],
focusedMessageIdx: undefined,
isCheckedOfSelectionId: {}
}
export type ThreadsState = {
allThreads: ChatThreads;
currentThreadId: string; // intended for internal use only
}
export type ThreadStreamState = {
[threadId: string]: undefined | {
error?: { message: string, fullError: Error | null, };
messageSoFar?: string;
reasoningSoFar?: string;
streamingToken?: string;
}
}
const newThreadObject = () => {
const now = new Date().toISOString()
return {
id: generateUuid(),
createdAt: now,
lastModified: now,
messages: [],
state: defaultThreadState,
} satisfies ChatThreads[string]
}
export const THREAD_STORAGE_KEY = 'void.chatThreadStorage'
type ChatMode = 'agent' | 'chat'
export interface IChatThreadService {
readonly _serviceBrand: undefined;
readonly state: ThreadsState;
readonly streamState: ThreadStreamState;
onDidChangeCurrentThread: Event<void>;
onDidChangeStreamState: Event<{ threadId: string }>
getCurrentThread(): ChatThreads[string];
openNewThread(): void;
switchToThread(threadId: string): void;
// you can edit multiple messages
// the one you're currently editing is "focused", and we add items to that one when you press cmd+L.
getFocusedMessageIdx(): number | undefined;
isFocusingMessage(): boolean;
setFocusedMessageIdx(messageIdx: number | undefined): void;
// exposed getters/setters
getCurrentMessageState: (messageIdx: number) => UserMessageState
setCurrentMessageState: (messageIdx: number, newState: Partial<UserMessageState>) => void
getCurrentThreadState: () => ThreadType['state']
setCurrentThreadState: (newState: Partial<ThreadType['state']>) => void
// call to edit a message
editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise<void>;
// call to add a message
addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise<void>;
cancelStreaming(threadId: string): void;
dismissStreamError(threadId: string): void;
}
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
class ChatThreadService extends Disposable implements IChatThreadService {
_serviceBrand: undefined;
// this fires when the current thread changes at all (a switch of currentThread, or a message added to it, etc)
private readonly _onDidChangeCurrentThread = new Emitter<void>();
readonly onDidChangeCurrentThread: Event<void> = this._onDidChangeCurrentThread.event;
readonly streamState: ThreadStreamState = {}
private readonly _onDidChangeStreamState = new Emitter<{ threadId: string }>();
readonly onDidChangeStreamState: Event<{ threadId: string }> = this._onDidChangeStreamState.event;
state: ThreadsState // allThreads is persisted, currentThread is not
constructor(
@IStorageService private readonly _storageService: IStorageService,
@IVoidFileService private readonly _voidFileService: IVoidFileService,
@ILLMMessageService private readonly _llmMessageService: ILLMMessageService,
@IToolsService private readonly _toolsService: IToolsService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
const readThreads = this._readAllThreads() || {}
const allThreads = readThreads
this.state = {
allThreads: allThreads,
currentThreadId: null as unknown as string, // gets set in startNewThread()
}
// always be in a thread
this.openNewThread()
}
// !!! this is important for properly restoring URIs from storage
private _convertThreadDataFromStorage(threadsStr: string): ChatThreads {
return JSON.parse(threadsStr, (key, value) => {
if (value && typeof value === 'object' && value.$mid === 1) { //$mid is the MarshalledId. $mid === 1 means it is a URI
return URI.from(value);
}
return value;
});
}
private _readAllThreads(): ChatThreads | null {
const threadsStr = this._storageService.get(THREAD_STORAGE_KEY, StorageScope.APPLICATION);
if (!threadsStr) {
return null
}
return this._convertThreadDataFromStorage(threadsStr);
}
private _storeAllThreads(threads: ChatThreads) {
const serializedThreads = JSON.stringify(threads);
this._storageService.store(
THREAD_STORAGE_KEY,
serializedThreads,
StorageScope.APPLICATION,
StorageTarget.USER
);
}
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
this.state = {
...this.state,
...state
}
if (affectsCurrent)
this._onDidChangeCurrentThread.fire()
}
private _getAllSelections() {
const thread = this.getCurrentThread()
return thread.messages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _getSelectionsUpToMessageIdx(messageIdx: number) {
const thread = this.getCurrentThread()
const prevMessages = thread.messages.slice(0, messageIdx)
return prevMessages.flatMap(m => m.role === 'user' && m.selections || [])
}
private _setStreamState(threadId: string, state: Partial<NonNullable<ThreadStreamState[string]>>) {
this.streamState[threadId] = {
...this.streamState[threadId],
...state
}
this._onDidChangeStreamState.fire({ threadId })
}
// ---------- streaming ----------
private _finishStreamingTextMessage = (threadId: string, options: { content: string, reasoning?: string }, error?: { message: string, fullError: Error | null }) => {
// add assistant's message to chat history, and clear selection
this._addMessageToThread(threadId, { role: 'assistant', content: options.content, reasoning: options.reasoning || null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error })
}
async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) {
const thread = this.getCurrentThread()
if (thread.messages?.[messageIdx]?.role !== 'user') {
throw new Error(`Error: editing a message with role !=='user'`)
}
// get prev and curr selections before clearing the message
const prevSelns = this._getSelectionsUpToMessageIdx(messageIdx) // selections for previous messages
const currSelns = thread.messages[messageIdx].state.stagingSelections || [] // staging selections for the edited message
// clear messages up to the index
const slicedMessages = thread.messages.slice(0, messageIdx)
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
messages: slicedMessages
}
}
}, true)
// re-add the message and stream it
this.addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections: { prevSelns, currSelns } })
}
async addUserMessageAndStreamResponse({ userMessage, chatMode, chatSelections }: { userMessage: string, chatMode: ChatMode, chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) {
const thread = this.getCurrentThread()
const threadId = thread.id
// selections in all past chats, then in current chat (can have many duplicates here)
const prevSelns: StagingSelectionItem[] = chatSelections?.prevSelns ?? this._getAllSelections()
const currSelns: StagingSelectionItem[] = chatSelections?.currSelns ?? thread.state.stagingSelections
// add user's message to chat history
const instructions = userMessage
const userMessageContent = await chat_userMessageContent(instructions, currSelns)
const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService)
const userMessageFullContent = chat_userMessageContentWithAllFilesToo(userMessageContent, selectionsStr)
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
this._setStreamState(threadId, { error: undefined })
const tools: InternalToolInfo[] | undefined = (
chatMode === 'chat' ? undefined
: chatMode === 'agent' ? Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName])
: undefined)
// agent loop
const agentLoop = async () => {
let shouldSendAnotherMessage = true
let nMessagesSent = 0
while (shouldSendAnotherMessage) {
shouldSendAnotherMessage = false
nMessagesSent += 1
let res_: () => void
const awaitable = new Promise<void>((res, rej) => { res_ = res })
// replace last userMessage with userMessageFullContent (which contains all the files too)
const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m)))
const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user')
let messages = messages_
if (lastUserMsgIdx !== -1) { // should never be -1
messages = [
...messages.slice(0, lastUserMsgIdx),
{ role: 'user', content: userMessageFullContent },
...messages.slice(lastUserMsgIdx + 1, Infinity)]
}
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
useProviderFor: 'Ctrl+L',
logging: { loggingName: `Agent` },
messages: [
{ role: 'system', content: chat_systemMessage(this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)) },
...messages,
],
tools: tools,
onText: ({ fullText, fullReasoning }) => {
this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning })
},
onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => {
if ((toolCalls?.length ?? 0) === 0) {
this._finishStreamingTextMessage(threadId, { content: fullText, reasoning: fullReasoning })
}
else {
this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null })
this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message
// deal with the tool
const tool: ToolCallType | undefined = toolCalls?.[0]
if (!tool) {
res_()
return
}
const toolName = tool.name
// 1.
let toolResultVal: ToolCallReturnType[typeof toolName]
try {
const val = await this._toolsService.toolFns[toolName](tool.params)
toolResultVal = val
} catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, })
shouldSendAnotherMessage = true
res_()
return
}
// 2.
let toolResultStr: string
try {
toolResultStr = this._toolsService.toolResultToString[toolName](toolResultVal as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here
} catch (error) {
// treat as irrecoverable error
this._setStreamState(threadId, { error })
res_()
return
}
this._addMessageToThread(threadId, { role: 'tool', name: toolName, params: tool.params, id: tool.id, content: toolResultStr, result: { type: 'success', value: toolResultVal }, })
shouldSendAnotherMessage = true
res_()
}
},
onError: (error) => {
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }, error)
res_()
},
})
if (llmCancelToken === null) break
this._setStreamState(threadId, { streamingToken: llmCancelToken })
await awaitable
}
}
agentLoop() // DO NOT AWAIT THIS, add fn should resolve when we've added message (this lets us interrupt the agent loop correctly instead of waiting for it to resolve)
}
cancelStreaming(threadId: string) {
const llmCancelToken = this.streamState[threadId]?.streamingToken
if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken)
const messageSoFar = this.streamState[threadId]?.messageSoFar ?? ''
const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? ''
this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar })
}
dismissStreamError(threadId: string): void {
this._setStreamState(threadId, { error: undefined })
}
// ---------- the rest ----------
getCurrentThread(): ChatThreads[string] {
const state = this.state
const thread = state.allThreads[state.currentThreadId]
return thread
}
getFocusedMessageIdx() {
const thread = this.getCurrentThread()
// get the focusedMessageIdx
const focusedMessageIdx = thread.state.focusedMessageIdx
if (focusedMessageIdx === undefined) return;
// check that the message is actually being edited
const focusedMessage = thread.messages[focusedMessageIdx]
if (focusedMessage.role !== 'user') return;
if (!focusedMessage.state) return;
return focusedMessageIdx
}
isFocusingMessage() {
return this.getFocusedMessageIdx() !== undefined
}
switchToThread(threadId: string) {
this._setState({ currentThreadId: threadId }, true)
}
openNewThread() {
// if a thread with 0 messages already exists, switch to it
const { allThreads: currentThreads } = this.state
for (const threadId in currentThreads) {
if (currentThreads[threadId].messages.length === 0) {
this.switchToThread(threadId)
return
}
}
// otherwise, start a new thread
const newThread = newThreadObject()
// update state
const newThreads: ChatThreads = {
...currentThreads,
[newThread.id]: newThread
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
}
_addMessageToThread(threadId: string, message: ChatMessage) {
const { allThreads } = this.state
const oldThread = allThreads[threadId]
// update state and store it
const newThreads = {
...allThreads,
[oldThread.id]: {
...oldThread,
lastModified: new Date().toISOString(),
messages: [...oldThread.messages, message],
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
}
// sets the currently selected message (must be undefined if no message is selected)
setFocusedMessageIdx(messageIdx: number | undefined) {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
state: {
...thread.state,
focusedMessageIdx: messageIdx,
}
}
}
}, true)
}
// set message.state
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[threadId]: {
...thread,
messages: thread.messages.map((m, i) =>
i === messageIdx && m.role === 'user' ? {
...m,
state: {
...m.state,
...state
},
} : m
)
}
}
}, true)
}
// set thread.state
private _setCurrentThreadState(state: Partial<ThreadType['state']>): void {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
this._setState({
allThreads: {
...this.state.allThreads,
[thread.id]: {
...thread,
state: {
...thread.state,
...state
}
}
}
}, true)
}
getCurrentThreadState = () => {
const currentThread = this.getCurrentThread()
return currentThread.state
}
setCurrentThreadState = (newState: Partial<ThreadType['state']>) => {
this._setCurrentThreadState(newState)
}
// gets `staging` and `setStaging` of the currently focused element, given the index of the currently selected message (or undefined if no message is selected)
getCurrentMessageState(messageIdx: number): UserMessageState {
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
if (!currMessage || currMessage.role !== 'user') return defaultMessageState
return currMessage.state
}
setCurrentMessageState(messageIdx: number, newState: Partial<UserMessageState>) {
const currMessage = this.getCurrentThread()?.messages?.[messageIdx]
if (!currMessage || currMessage.role !== 'user') return
this._setCurrentMessageState(newState, messageIdx)
}
}
registerSingleton(IChatThreadService, ChatThreadService, InstantiationType.Eager);
@@ -0,0 +1,87 @@
import { URI } from '../../../../base/common/uri.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { AnthropicReasoning } from './sendLLMMessageTypes.js';
import { ToolName, ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = {
role: 'tool';
name: T; // internal use
paramsStr: string; // internal use
id: string; // apis require this tool use id
content: string; // give this result to LLM
// if rejected, don't show in chat
result:
| { type: 'success'; params: ToolCallParams[T]; value: ToolResultType[T], }
| { type: 'error'; params: ToolCallParams[T] | undefined; value: string }
| { type: 'rejected'; params: ToolCallParams[T] }
}
export type ToolRequestApproval<T extends ToolName> = {
role: 'tool_request';
name: T; // internal use
params: ToolCallParams[T]; // internal use
paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params)
id: string; // proposed tool's id
}
// WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors.
export type ChatMessage =
| {
role: 'user';
content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty)
displayContent: string; // content displayed to user - allowed to be '', will be ignored
selections: StagingSelectionItem[] | null; // the user's selection
state: {
stagingSelections: StagingSelectionItem[];
isBeingEdited: boolean;
}
} | {
role: 'assistant';
content: string; // content received from LLM - allowed to be '', will be replaced with (empty)
reasoning: string; // reasoning from the LLM, used for step-by-step thinking
anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning
}
| ToolMessage<ToolName>
| ToolRequestApproval<ToolName>
// one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text)
export type CodeSelection = {
type: 'Selection';
fileURI: URI;
language: string;
selectionStr: string;
range: IRange;
state: {
isOpened: boolean;
wasAddedAsCurrentFile: boolean;
};
}
export type FileSelection = {
type: 'File';
fileURI: URI;
language: string;
selectionStr: null;
range: null;
state: {
isOpened: boolean;
wasAddedAsCurrentFile: boolean;
};
}
export type StagingSelectionItem = CodeSelection | FileSelection
export type CodespanLocationLink = {
uri: URI, // we handle serialization for this
displayText: string,
selection?: { // store as JSON so dont have to worry about serialization
startLineNumber: number
startColumn: number,
endLineNumber: number
endColumn: number,
} | undefined
} | null
@@ -0,0 +1,8 @@
export const acceptBg = '#1a7431'
export const acceptAllBg = '#1e8538'
export const acceptBorder = '1px solid #145626'
export const rejectBg = '#b42331'
export const rejectAllBg = '#cf2838'
export const rejectBorder = '1px solid #8e1c27'
export const buttonFontSize = '11px'
export const buttonTextColor = 'white'
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { OnText } from '../../common/llmMessageTypes.js'
import { OnText } from '../sendLLMMessageTypes.js'
import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js'
class SurroundingsRemover {
@@ -171,6 +171,9 @@ export type ExtractedSearchReplaceBlock = {
}
// JS substring swaps indices, so "ab".substr(1,0) will NOT be '', it will be 'a'!
const voidSubstr = (str: string, start: number, end: number) => end < start ? '' : str.substring(start, end)
const endsWithAnyPrefixOf = (str: string, anyPrefix: string) => {
// for each prefix
for (let i = anyPrefix.length; i >= 1; i--) { // i >= 1 because must not be empty string
@@ -187,7 +190,6 @@ export const extractSearchReplaceBlocks = (str: string) => {
const DIVIDER_ = '\n' + DIVIDER + `\n`
// logic for FINAL_ is slightly more complicated - should be '\n' + FINAL, but that ignores if the final output is empty
const blocks: ExtractedSearchReplaceBlock[] = []
let i = 0 // search i and beyond (this is done by plain index, not by line number. much simpler this way)
@@ -196,41 +198,42 @@ export const extractSearchReplaceBlocks = (str: string) => {
if (origStart === -1) { return blocks }
origStart += ORIGINAL_.length
i = origStart
// wrote <<<< ORIGINAL
// wrote <<<< ORIGINAL\n
let dividerStart = str.indexOf(DIVIDER_, i)
if (dividerStart === -1) { // if didnt find DIVIDER_, either writing originalStr or DIVIDER_ right now
const isWritingDIVIDER = endsWithAnyPrefixOf(str, DIVIDER_)
const writingDIVIDERlen = endsWithAnyPrefixOf(str, DIVIDER_)?.length ?? 0
blocks.push({
orig: str.substring(origStart, str.length - (isWritingDIVIDER?.length ?? 0)),
orig: voidSubstr(str, origStart, str.length - writingDIVIDERlen),
final: '',
state: 'writingOriginal'
})
return blocks
}
const origStrDone = str.substring(origStart, dividerStart)
const origStrDone = voidSubstr(str, origStart, dividerStart)
dividerStart += DIVIDER_.length
i = dividerStart
// wrote =====
// wrote \n=====\n
const fullFINALStart = str.indexOf(FINAL, i)
const fullFINALStart_ = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive
const matchedFullFINAL_ = fullFINALStart_ !== -1 && fullFINALStart === fullFINALStart_ + 1 // this logic is really important, otherwise we might look for FINAL_ at a much later part of the string
const finalStartA = str.indexOf(FINAL, i)
const finalStartB = str.indexOf('\n' + FINAL, i) // go with B if possible, else fallback to A, it's more permissive
const FINAL_ = finalStartB !== -1 ? '\n' + FINAL : FINAL
let finalStart = finalStartB !== -1 ? finalStartB : finalStartA
if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL_ right now
const isWritingFINAL = endsWithAnyPrefixOf(str, FINAL_)
let finalStart = matchedFullFINAL_ ? fullFINALStart_ : fullFINALStart
if (finalStart === -1) { // if didnt find FINAL_, either writing finalStr or FINAL or FINAL_ right now
const writingFINALlen = endsWithAnyPrefixOf(str, FINAL)?.length ?? 0
const writingFINALlen_ = endsWithAnyPrefixOf(str, '\n' + FINAL)?.length ?? 0 // this gets priority
const usingWritingFINALlen = Math.max(writingFINALlen, writingFINALlen_)
blocks.push({
orig: origStrDone,
final: str.substring(dividerStart, str.length - (isWritingFINAL?.length ?? 0)),
final: voidSubstr(str, dividerStart, str.length - usingWritingFINALlen),
state: 'writingFinal'
})
return blocks
}
const finalStrDone = str.substring(dividerStart, finalStart)
finalStart += FINAL_.length
const usingFINAL = matchedFullFINAL_ ? '\n' + FINAL : FINAL
const finalStrDone = voidSubstr(str, dividerStart, finalStart)
finalStart += usingFINAL.length
i = finalStart
// wrote >>>>> FINAL
@@ -247,9 +250,6 @@ export const extractSearchReplaceBlocks = (str: string) => {
// could simplify this - this assumes we can never add a tag without committing it to the user's screen, but that's not true
export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string, string]): OnText => {
let latestAddIdx = 0 // exclusive index in fullText_
@@ -264,7 +264,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
onText_(params)
}
const newOnText: OnText = ({ fullText: fullText_ }) => {
const newOnText: OnText = ({ fullText: fullText_, ...p }) => {
// until found the first think tag, keep adding to fullText
if (!foundTag1) {
const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0])
@@ -282,7 +282,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
fullTextSoFar += fullText_.substring(0, tag1Index)
// Update latestAddIdx to after the first tag
latestAddIdx = tag1Index + thinkTags[0].length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@@ -290,7 +290,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
// add the text to fullText
fullTextSoFar = fullText_
latestAddIdx = fullText_.length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@@ -314,7 +314,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index)
// Update latestAddIdx to after the second tag
latestAddIdx = tag2Index + thinkTags[1].length
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@@ -327,7 +327,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
latestAddIdx = fullText_.length
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
return
}
@@ -340,7 +340,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string
latestAddIdx = fullText_.length
}
onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar })
}
return newOnText
@@ -357,3 +357,220 @@ export const extractReasoningOnFinalMessage = (fullText_: string, thinkTags: [st
const fullText = fullText_.substring(0, tag1Idx) + fullText_.substring(tag2Idx + thinkTags[1].length, Infinity)
return { fullText, fullReasoning }
}
// const tests: [string, { shape: Partial<ExtractedSearchReplaceBlock>[] }][] = [[
// `\
// \`\`\`
// <<<<<<< ORIGINA`, { shape: [] }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL`, { shape: [], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A`, { shape: [{ state: 'writingOriginal', orig: 'A' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// `, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// ===`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// ======`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======`, { shape: [{ state: 'writingOriginal', orig: 'A\nB' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// `, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: '' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// >>>>>>> UPDAT`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: '' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// >>>>>>> UPDATED`, { shape: [{ state: 'done', orig: 'A\nB', final: '' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// >>>>>>> UPDATED
// \`\`\``, { shape: [{ state: 'done', orig: 'A\nB', final: '' }], }
// ],
// // alternatively
// [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// X`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X' }], }
// ],
// [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// X
// Y`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X\nY' }], }
// ],
// [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// X
// Y
// `, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X\nY' }], }
// ],
// [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// X
// Y
// >>>>>>> UPDAT`, { shape: [{ state: 'writingFinal', orig: 'A\nB', final: 'X\nY' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// X
// Y
// >>>>>>> UPDATED`, { shape: [{ state: 'done', orig: 'A\nB', final: 'X\nY' }], }
// ], [
// `\
// \`\`\`
// <<<<<<< ORIGINAL
// A
// B
// =======
// X
// Y
// >>>>>>> UPDATED
// \`\`\``, { shape: [{ state: 'done', orig: 'A\nB', final: 'X\nY' }], }
// ]]
// function runTests() {
// let passedTests = 0;
// let failedTests = 0;
// for (let i = 0; i < tests.length; i++) {
// const [input, expected] = tests[i];
// const result = extractSearchReplaceBlocks(input);
// // Compare result with expected shape
// let passed = true;
// if (result.length !== expected.shape.length) {
// passed = false;
// } else {
// for (let j = 0; j < result.length; j++) { // block
// const expectedItem = expected.shape[j];
// const resultItem = result[j];
// if ((expectedItem.state !== undefined) && (expectedItem.state !== resultItem.state) ||
// (expectedItem.orig !== undefined) && (expectedItem.orig !== resultItem.orig) ||
// (expectedItem.final !== undefined) && (expectedItem.final !== resultItem.final)) {
// passed = false;
// break;
// }
// }
// }
// if (passed) {
// passedTests++;
// console.log(`Test ${i + 1} passed`);
// } else {
// failedTests++;
// console.log(`Test ${i + 1} failed`);
// console.log('Input:', input)
// console.log(`Expected:`, expected.shape);
// console.log(`Got:`, result);
// }
// }
// console.log(`Total: ${tests.length}, Passed: ${passedTests}, Failed: ${failedTests}`);
// return failedTests === 0;
// }
// runTests()
@@ -0,0 +1,197 @@
// /*--------------------------------------------------------------------------------------
// * Copyright 2025 Glass Devtools, Inc. All rights reserved.
// * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
// *--------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js';
import { ILanguageService } from '../../../../../editor/common/languages/language.js';
import { separateOutFirstLine } from './util.js';
// this works better than model.getLanguageId()
export function detectLanguage(languageService: ILanguageService, opts: { uri: URI | null, fileContents: string | undefined }) {
const firstLine = opts.fileContents ? separateOutFirstLine(opts.fileContents)?.[0] : undefined
const fullLang = languageService.createByFilepathOrFirstLine(opts.uri, firstLine)
return fullLang.languageId || 'plaintext'
}
// --- conversions
export const convertToVscodeLang = (languageService: ILanguageService, markdownLang: string) => {
if (markdownLang in markdownLangToVscodeLang)
return markdownLangToVscodeLang[markdownLang]
const { languageId } = languageService.createById(markdownLang)
return languageId
}
// // eg "bash" -> "shell"
const markdownLangToVscodeLang: { [key: string]: string } = {
// Web Technologies
'html': 'html',
'css': 'css',
'scss': 'scss',
'sass': 'scss',
'less': 'less',
'javascript': 'typescript',
'js': 'typescript', // use more general renderer
'jsx': 'typescriptreact',
'typescript': 'typescript',
'ts': 'typescript',
'tsx': 'typescriptreact',
'json': 'json',
'jsonc': 'json',
// Programming Languages
'python': 'python',
'py': 'python',
'java': 'java',
'cpp': 'cpp',
'c++': 'cpp',
'c': 'c',
'csharp': 'csharp',
'cs': 'csharp',
'c#': 'csharp',
'go': 'go',
'golang': 'go',
'rust': 'rust',
'rs': 'rust',
'ruby': 'ruby',
'rb': 'ruby',
'php': 'php',
'shell': 'shellscript', // this is important
'bash': 'shellscript',
'sh': 'shellscript',
'zsh': 'shellscript',
// Markup and Config
'markdown': 'markdown',
'md': 'markdown',
'xml': 'xml',
'svg': 'xml',
'yaml': 'yaml',
'yml': 'yaml',
'ini': 'ini',
'toml': 'ini',
// Database and Query Languages
'sql': 'sql',
'mysql': 'sql',
'postgresql': 'sql',
'graphql': 'graphql',
'gql': 'graphql',
// Others
'dockerfile': 'dockerfile',
'docker': 'dockerfile',
'makefile': 'makefile',
'plaintext': 'plaintext',
'text': 'plaintext'
};
// // eg ".ts" -> "typescript"
// const fileExtensionToVscodeLanguage: { [key: string]: string } = {
// // Web
// 'html': 'html',
// 'htm': 'html',
// 'css': 'css',
// 'scss': 'scss',
// 'less': 'less',
// 'js': 'javascript',
// 'jsx': 'javascript',
// 'ts': 'typescript',
// 'tsx': 'typescript',
// 'json': 'json',
// 'jsonc': 'json',
// // Programming Languages
// 'py': 'python',
// 'java': 'java',
// 'cpp': 'cpp',
// 'cc': 'cpp',
// 'c': 'c',
// 'h': 'cpp',
// 'hpp': 'cpp',
// 'cs': 'csharp',
// 'go': 'go',
// 'rs': 'rust',
// 'rb': 'ruby',
// 'php': 'php',
// 'sh': 'shell',
// 'bash': 'shell',
// 'zsh': 'shell',
// // Markup/Config
// 'md': 'markdown',
// 'markdown': 'markdown',
// 'xml': 'xml',
// 'svg': 'xml',
// 'yaml': 'yaml',
// 'yml': 'yaml',
// 'ini': 'ini',
// 'toml': 'ini',
// // Other
// 'sql': 'sql',
// 'graphql': 'graphql',
// 'gql': 'graphql',
// 'dockerfile': 'dockerfile',
// 'docker': 'dockerfile',
// 'mk': 'makefile',
// // Config Files and Dot Files
// 'npmrc': 'ini',
// 'env': 'ini',
// 'gitignore': 'ignore',
// 'dockerignore': 'ignore',
// 'eslintrc': 'json',
// 'babelrc': 'json',
// 'prettierrc': 'json',
// 'stylelintrc': 'json',
// 'editorconfig': 'ini',
// 'htaccess': 'apacheconf',
// 'conf': 'ini',
// 'config': 'ini',
// // Package Files
// 'package': 'json',
// 'package-lock': 'json',
// 'gemfile': 'ruby',
// 'podfile': 'ruby',
// 'rakefile': 'ruby',
// // Build Systems
// 'cmake': 'cmake',
// 'makefile': 'makefile',
// 'gradle': 'groovy',
// // Shell Scripts
// 'bashrc': 'shell',
// 'zshrc': 'shell',
// 'fish': 'shell',
// // Version Control
// 'gitconfig': 'ini',
// 'hgrc': 'ini',
// 'svnconfig': 'ini',
// // Web Server
// 'nginx': 'nginx',
// // Misc Config
// 'properties': 'properties',
// 'cfg': 'ini',
// 'reg': 'ini'
// };
// export function filenameToVscodeLanguage(filename: string): string | undefined {
// const ext = filename.toLowerCase().split('.').pop();
// if (!ext) return undefined;
// return fileExtensionToVscodeLanguage[ext];
// }
@@ -0,0 +1,18 @@
export const separateOutFirstLine = (content: string): [string, string] | [string, undefined] => {
const newLineIdx = content.indexOf('\r\n')
if (newLineIdx !== -1) {
const A = content.substring(0, newLineIdx)
const B = content.substring(newLineIdx + 2, Infinity);
return [A, B]
}
const newLineIdx2 = content.indexOf('\n')
if (newLineIdx2 !== -1) {
const A = content.substring(0, newLineIdx2)
const B = content.substring(newLineIdx2 + 1, Infinity);
return [A, B]
}
return [content, undefined]
}
@@ -3,12 +3,11 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js';
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { localize2 } from '../../../../nls.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { registerAction2, Action2 } from '../../../../platform/actions/common/actions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
@@ -0,0 +1,726 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js';
export const defaultModelsOfProvider = {
openAI: [ // https://platform.openai.com/docs/models/gp
'o3-mini',
'o1',
'o1-mini',
'gpt-4o',
'gpt-4o-mini',
],
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
'claude-3-7-sonnet-latest',
'claude-3-5-sonnet-latest',
'claude-3-5-haiku-latest',
'claude-3-opus-latest',
],
xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1
'grok-2-latest',
'grok-3-latest',
],
gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini
'gemini-2.0-flash',
'gemini-1.5-flash',
'gemini-1.5-pro',
'gemini-1.5-flash-8b',
'gemini-2.0-flash-thinking-exp',
],
deepseek: [ // https://api-docs.deepseek.com/quick_start/pricing
'deepseek-chat',
'deepseek-reasoner',
],
ollama: [ // autodetected
],
vLLM: [ // autodetected
],
openRouter: [ // https://openrouter.ai/models
'anthropic/claude-3.7-sonnet:thinking',
'anthropic/claude-3.7-sonnet',
'anthropic/claude-3.5-sonnet',
'deepseek/deepseek-r1',
'mistralai/codestral-2501',
'qwen/qwen-2.5-coder-32b-instruct',
],
groq: [ // https://console.groq.com/docs/models
'qwen-qwq-32b',
'llama-3.3-70b-versatile',
'llama-3.1-8b-instant',
// 'qwen-2.5-coder-32b', // preview mode (experimental)
],
// not supporting mistral right now- it's last on Void usage, and a huge pain to set up since it's nonstandard (it supports codestral FIM but it's on v1/fim/completions, etc)
// mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
// 'codestral-latest',
// 'mistral-large-latest',
// 'ministral-3b-latest',
// 'ministral-8b-latest',
// ],
openAICompatible: [], // fallback
} as const satisfies Record<ProviderName, string[]>
type ModelOptions = {
contextWindow: number; // input tokens // <-- UNUSED
maxOutputTokens: number | null; // output tokens // <-- UNUSED
cost: { // <-- UNUSED
input: number;
output: number;
cache_read?: number;
cache_write?: number;
}
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated';
supportsTools: false | 'anthropic-style' | 'openai-style';
supportsFIM: boolean;
reasoningCapabilities: false | {
readonly supportsReasoning: true;
// reasoning options if supports reasoning
readonly canTurnOffReasoning: boolean; // whether or not the user can disable reasoning mode (false if the model only supports reasoning)
readonly canIOReasoning: boolean; // whether or not the model actually outputs reasoning (eg o1 lets us control reasoning but not output it)
readonly reasoningMaxOutputTokens?: number; // overrides normal maxOutputTokens // <-- UNUSED (except anthropic)
readonly reasoningBudgetSlider?: { type: 'slider'; min: number; max: number; default: number };
// options related specifically to model output
// you are allowed to not include openSourceThinkTags if it's not open source (no such cases as of writing)
// if it's open source, put the think tags here so we parse them out in e.g. ollama
readonly openSourceThinkTags?: [string, string];
};
}
type ProviderReasoningIOSettings = {
// include this in payload to get reasoning
input?: { includeInPayload?: (reasoningState: SendableReasoningInfo) => null | { [key: string]: any }, };
// nameOfFieldInDelta: reasoning output is in response.choices[0].delta[deltaReasoningField]
// needsManualParse: whether we must manually parse out the <think> tags
output?:
| { nameOfFieldInDelta?: string, needsManualParse?: undefined, }
| { nameOfFieldInDelta?: undefined, needsManualParse?: true, };
}
type ProviderSettings = {
providerReasoningIOSettings?: ProviderReasoningIOSettings; // input/output settings around thinking (allowed to be empty) - only applied if the model supports reasoning output
modelOptions: { [key: string]: ModelOptions };
modelOptionsFallback: (modelName: string) => (ModelOptions & { modelName: string }) | null;
}
const modelOptionsDefaults: ModelOptions = {
contextWindow: 32_000, // unused
maxOutputTokens: null, // unused
cost: { input: 0, output: 0 }, // unused
supportsSystemMessage: false,
supportsTools: false,
supportsFIM: false,
reasoningCapabilities: false,
}
const openSourceModelOptions_assumingOAICompat = {
'deepseekR1': {
supportsFIM: false,
supportsSystemMessage: false,
supportsTools: false,
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
},
'deepseekCoderV2': {
supportsFIM: false,
supportsSystemMessage: false, // unstable
supportsTools: false,
reasoningCapabilities: false,
},
'codestral': {
supportsFIM: true,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
// llama
'llama3': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'llama3.1': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'llama3.2': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'llama3.3': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
// qwen
'qwen2.5coder': {
supportsFIM: true,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'qwq': {
supportsFIM: false, // no FIM, yes reasoning
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
},
// FIM only
'starcoder2': {
supportsFIM: true,
supportsSystemMessage: false,
supportsTools: false,
reasoningCapabilities: false,
},
'codegemma:2b': {
supportsFIM: true,
supportsSystemMessage: false,
supportsTools: false,
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: Partial<ModelOptions> }
const extensiveModelFallback: ProviderSettings['modelOptionsFallback'] = (modelName) => {
const toFallback = (opts: Omit<ModelOptions, 'cost'>): ModelOptions & { modelName: string } => {
return {
modelName,
...opts,
supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false,
cost: { input: 0, output: 0 },
}
}
if (modelName.includes('gpt-4o')) return toFallback(openAIModelOptions['gpt-4o'])
if (modelName.includes('claude-3-5') || modelName.includes('claude-3.5')) return toFallback(anthropicModelOptions['claude-3-5-sonnet-20241022'])
if (modelName.includes('claude')) return toFallback(anthropicModelOptions['claude-3-7-sonnet-20250219'])
if (modelName.includes('grok')) return toFallback(xAIModelOptions['grok-2'])
if (modelName.includes('deepseek-r1') || modelName.includes('deepseek-reasoner')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekR1, contextWindow: 32_000, maxOutputTokens: 4_096, })
if (modelName.includes('deepseek')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.deepseekCoderV2, contextWindow: 32_000, maxOutputTokens: 4_096, })
if (modelName.includes('llama3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.llama3, contextWindow: 32_000, maxOutputTokens: 4_096, })
if (modelName.includes('qwen') && modelName.includes('2.5') && modelName.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'], contextWindow: 32_000, maxOutputTokens: 4_096, })
if (modelName.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral, contextWindow: 32_000, maxOutputTokens: 4_096, })
if (/\bo1\b/.test(modelName) || /\bo3\b/.test(modelName)) return toFallback(openAIModelOptions['o1'])
return toFallback(modelOptionsDefaults)
}
// ---------------- ANTHROPIC ----------------
const anthropicModelOptions = {
'claude-3-7-sonnet-20250219': { // https://docs.anthropic.com/en/docs/about-claude/models/all-models#model-comparison-table
contextWindow: 200_000,
maxOutputTokens: 8_192,
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: {
supportsReasoning: true,
canTurnOffReasoning: true,
canIOReasoning: true,
reasoningMaxOutputTokens: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
},
},
'claude-3-5-sonnet-20241022': {
contextWindow: 200_000,
maxOutputTokens: 8_192,
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
},
'claude-3-5-haiku-20241022': {
contextWindow: 200_000,
maxOutputTokens: 8_192,
cost: { input: 0.80, cache_read: 0.08, cache_write: 1.00, output: 4.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
},
'claude-3-opus-20240229': {
contextWindow: 200_000,
maxOutputTokens: 4_096,
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 75.00 },
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
},
'claude-3-sonnet-20240229': { // no point of using this, but including this for people who put it in
contextWindow: 200_000, cost: { input: 3.00, output: 15.00 },
maxOutputTokens: 4_096,
supportsFIM: false,
supportsSystemMessage: 'separated',
supportsTools: 'anthropic-style',
reasoningCapabilities: false,
}
} as const satisfies { [s: string]: ModelOptions }
const anthropicSettings: ProviderSettings = {
providerReasoningIOSettings: {
input: {
includeInPayload: (reasoningInfo) => {
if (reasoningInfo?.type === 'budgetEnabled') {
return { thinking: { type: 'enabled', budget_tokens: reasoningInfo.reasoningBudget } }
}
return null
}
},
},
modelOptions: anthropicModelOptions,
modelOptionsFallback: (modelName) => {
let fallbackName: keyof typeof anthropicModelOptions | null = null
if (modelName.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219'
if (modelName.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022'
if (modelName.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022'
if (modelName.includes('claude-3-opus')) fallbackName = 'claude-3-opus-20240229'
if (modelName.includes('claude-3-sonnet')) fallbackName = 'claude-3-sonnet-20240229'
if (fallbackName) return { modelName: fallbackName, ...anthropicModelOptions[fallbackName] }
return { modelName, ...modelOptionsDefaults, maxOutputTokens: 4_096 }
},
}
// ---------------- OPENAI ----------------
const openAIModelOptions = { // https://platform.openai.com/docs/pricing
'o1': {
contextWindow: 128_000,
maxOutputTokens: 100_000,
cost: { input: 15.00, cache_read: 7.50, output: 60.00, },
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: 'developer-role',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false }, // it doesn't actually output reasoning, but our logic is fine with it
},
'o3-mini': {
contextWindow: 200_000,
maxOutputTokens: 100_000,
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: 'developer-role',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false },
},
'gpt-4o': {
contextWindow: 128_000,
maxOutputTokens: 16_384,
cost: { input: 2.50, cache_read: 1.25, output: 10.00, },
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'o1-mini': {
contextWindow: 128_000,
maxOutputTokens: 65_536,
cost: { input: 1.10, cache_read: 0.55, output: 4.40, },
supportsFIM: false,
supportsTools: false,
supportsSystemMessage: false, // does not support any system
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false },
},
'gpt-4o-mini': {
contextWindow: 128_000,
maxOutputTokens: 16_384,
cost: { input: 0.15, cache_read: 0.075, output: 0.60, },
supportsFIM: false,
supportsTools: 'openai-style',
supportsSystemMessage: 'system-role', // ??
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: ModelOptions }
const openAISettings: ProviderSettings = {
modelOptions: openAIModelOptions,
modelOptionsFallback: (modelName) => {
let fallbackName: keyof typeof openAIModelOptions | null = null
if (modelName.includes('o1')) { fallbackName = 'o1' }
if (modelName.includes('o3-mini')) { fallbackName = 'o3-mini' }
if (modelName.includes('gpt-4o')) { fallbackName = 'gpt-4o' }
if (fallbackName) return { modelName: fallbackName, ...openAIModelOptions[fallbackName] }
return null
}
}
// ---------------- XAI ----------------
const xAIModelOptions = {
'grok-2': {
contextWindow: 131_072,
maxOutputTokens: null, // 131_072,
cost: { input: 2.00, output: 10.00 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: ModelOptions }
const xAISettings: ProviderSettings = {
modelOptions: xAIModelOptions,
modelOptionsFallback: (modelName) => {
let fallbackName: keyof typeof xAIModelOptions | null = null
if (modelName.includes('grok-2')) fallbackName = 'grok-2'
if (fallbackName) return { modelName: fallbackName, ...xAIModelOptions[fallbackName] }
return null
}
}
// ---------------- GEMINI ----------------
const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
'gemini-2.0-flash': {
contextWindow: 1_048_576,
maxOutputTokens: null, // 8_192,
cost: { input: 0.10, output: 0.40 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style', // we are assuming OpenAI SDK when calling gemini
reasoningCapabilities: false,
},
'gemini-2.0-flash-lite-preview-02-05': {
contextWindow: 1_048_576,
maxOutputTokens: null, // 8_192,
cost: { input: 0.075, output: 0.30 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'gemini-1.5-flash': {
contextWindow: 1_048_576,
maxOutputTokens: null, // 8_192,
cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'gemini-1.5-pro': {
contextWindow: 2_097_152,
maxOutputTokens: null, // 8_192,
cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'gemini-1.5-flash-8b': {
contextWindow: 1_048_576,
maxOutputTokens: null, // 8_192,
cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
} as const satisfies { [s: string]: ModelOptions }
const geminiSettings: ProviderSettings = {
modelOptions: geminiModelOptions,
modelOptionsFallback: (modelName) => { return null }
}
// ---------------- DEEPSEEK API ----------------
const deepseekModelOptions = {
'deepseek-chat': {
...openSourceModelOptions_assumingOAICompat.deepseekR1,
contextWindow: 64_000, // https://api-docs.deepseek.com/quick_start/pricing
maxOutputTokens: null, // 8_000,
cost: { cache_read: .07, input: .27, output: 1.10, },
},
'deepseek-reasoner': {
...openSourceModelOptions_assumingOAICompat.deepseekCoderV2,
contextWindow: 64_000,
maxOutputTokens: null, // 8_000,
cost: { cache_read: .14, input: .55, output: 2.19, },
},
} as const satisfies { [s: string]: ModelOptions }
const deepseekSettings: ProviderSettings = {
modelOptions: deepseekModelOptions,
providerReasoningIOSettings: {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
output: { nameOfFieldInDelta: 'reasoning_content' },
},
modelOptionsFallback: (modelName) => { return null }
}
// ---------------- GROQ ----------------
const groqModelOptions = { // https://console.groq.com/docs/models, https://groq.com/pricing/
'llama-3.3-70b-versatile': {
contextWindow: 128_000,
maxOutputTokens: null, // 32_768,
cost: { input: 0.59, output: 0.79 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'llama-3.1-8b-instant': {
contextWindow: 128_000,
maxOutputTokens: null, // 8_192,
cost: { input: 0.05, output: 0.08 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'qwen-2.5-coder-32b': {
contextWindow: 128_000,
maxOutputTokens: null, // not specified?
cost: { input: 0.79, output: 0.79 },
supportsFIM: false, // unfortunately looks like no FIM support on groq
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'qwen-qwq-32b': { // https://huggingface.co/Qwen/QwQ-32B
contextWindow: 128_000,
maxOutputTokens: null, // not specified?
cost: { input: 0.29, output: 0.39 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] }, // we're using reasoning_format:parsed so really don't need to know openSourceThinkTags
},
} as const satisfies { [s: string]: ModelOptions }
const groqSettings: ProviderSettings = {
providerReasoningIOSettings: {
input: {
includeInPayload: (reasoningInfo) => {
if (reasoningInfo?.type === 'budgetEnabled') {
return { reasoning_format: 'parsed' }
}
return null
}
},
output: { nameOfFieldInDelta: 'reasoning' },
}, // Must be set to either parsed or hidden when using tool calling https://console.groq.com/docs/reasoning
modelOptions: groqModelOptions,
modelOptionsFallback: (modelName) => { return null }
}
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
const vLLMSettings: ProviderSettings = {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
modelOptions: {},
}
const ollamaSettings: ProviderSettings = {
// reasoning: we need to filter out reasoning <think> tags manually
providerReasoningIOSettings: { output: { needsManualParse: true }, },
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
modelOptions: {},
}
const openaiCompatible: ProviderSettings = {
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
modelOptions: {},
}
// ---------------- OPENROUTER ----------------
const openRouterModelOptions_assumingOpenAICompat = {
'deepseek/deepseek-r1': {
...openSourceModelOptions_assumingOAICompat.deepseekR1,
contextWindow: 128_000,
maxOutputTokens: null,
cost: { input: 0.8, output: 2.4 },
},
'anthropic/claude-3.7-sonnet:thinking': {
contextWindow: 200_000,
maxOutputTokens: null,
cost: { input: 3.00, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: { // same as anthropic, see above
supportsReasoning: true,
canTurnOffReasoning: false,
canIOReasoning: true,
reasoningMaxOutputTokens: 64_000,
reasoningBudgetSlider: { type: 'slider', min: 1024, max: 32_000, default: 1024 }, // they recommend batching if max > 32_000
},
},
'anthropic/claude-3.7-sonnet': {
contextWindow: 200_000,
maxOutputTokens: null,
cost: { input: 3.00, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false, // stupidly, openrouter separates thinking from non-thinking
},
'anthropic/claude-3.5-sonnet': {
contextWindow: 200_000,
maxOutputTokens: null,
cost: { input: 3.00, output: 15.00 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'mistralai/codestral-2501': {
...openSourceModelOptions_assumingOAICompat.codestral,
contextWindow: 256_000,
maxOutputTokens: null,
cost: { input: 0.3, output: 0.9 },
supportsTools: 'openai-style',
reasoningCapabilities: false,
},
'qwen/qwen-2.5-coder-32b-instruct': {
...openSourceModelOptions_assumingOAICompat['qwen2.5coder'],
contextWindow: 33_000,
maxOutputTokens: null,
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
cost: { input: 0.07, output: 0.16 },
},
'qwen/qwq-32b': {
...openSourceModelOptions_assumingOAICompat['qwq'],
contextWindow: 33_000,
maxOutputTokens: null,
supportsTools: false, // openrouter qwen doesn't seem to support tools...?
cost: { input: 0.07, output: 0.16 },
}
} as const satisfies { [s: string]: ModelOptions }
const openRouterSettings: ProviderSettings = {
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
providerReasoningIOSettings: {
input: {
includeInPayload: (reasoningInfo) => {
if (reasoningInfo?.type === 'budgetEnabled') {
return {
reasoning: {
max_tokens: reasoningInfo.reasoningBudget
}
}
}
return null
}
},
output: { nameOfFieldInDelta: 'reasoning' },
},
modelOptions: openRouterModelOptions_assumingOpenAICompat,
// TODO!!! send a query to openrouter to get the price, etc.
modelOptionsFallback: (modelName) => extensiveModelFallback(modelName),
}
// ---------------- model settings of everything above ----------------
const modelSettingsOfProvider: { [providerName in ProviderName]: ProviderSettings } = {
openAI: openAISettings,
anthropic: anthropicSettings,
xAI: xAISettings,
gemini: geminiSettings,
// open source models
deepseek: deepseekSettings,
groq: groqSettings,
// open source models + providers (mixture of everything)
openRouter: openRouterSettings,
vLLM: vLLMSettings,
ollama: ollamaSettings,
openAICompatible: openaiCompatible,
// googleVertex: {},
// microsoftAzure: {},
} as const
// ---------------- exports ----------------
// returns the capabilities and the adjusted modelName if it was a fallback
export const getModelCapabilities = (providerName: ProviderName, modelName: string): ModelOptions & { modelName: string; isUnrecognizedModel: boolean } => {
const { modelOptions, modelOptionsFallback } = modelSettingsOfProvider[providerName]
if (modelName in modelOptions) return { modelName, ...modelOptions[modelName], isUnrecognizedModel: false }
const result = modelOptionsFallback(modelName)
if (result) return { ...result, isUnrecognizedModel: false }
return { modelName, ...modelOptionsDefaults, isUnrecognizedModel: true }
}
// non-model settings
export const getProviderCapabilities = (providerName: ProviderName) => {
const { providerReasoningIOSettings } = modelSettingsOfProvider[providerName]
return { providerReasoningIOSettings }
}
export type SendableReasoningInfo = {
type: 'budgetEnabled',
isReasoningEnabled: true,
reasoningBudget: number,
} | null
export const getIsResoningEnabledState = (
providerName: ProviderName,
modelName: string,
modelSelectionOptions: ModelSelectionOptions | undefined,
) => {
const { supportsReasoning } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {}
if (!supportsReasoning) return false
const defaultEnabledVal = true // if can't toggle reasoning, then this must be true. just true as default
const isReasoningEnabled = modelSelectionOptions?.reasoningEnabled ?? defaultEnabledVal
return isReasoningEnabled
}
// used to force reasoning state (complex) into something simple we can just read from when sending a message
export const getSendableReasoningInfo = (
providerName: ProviderName,
modelName: string,
modelSelectionOptions: ModelSelectionOptions | undefined,
): SendableReasoningInfo => {
const { canIOReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).reasoningCapabilities || {}
if (!canIOReasoning) return null
const isReasoningEnabled = getIsResoningEnabledState(providerName, modelName, modelSelectionOptions)
if (!isReasoningEnabled) return null
// check for reasoning budget
const reasoningBudget = reasoningBudgetSlider?.type === 'slider' ? modelSelectionOptions?.reasoningBudget ?? reasoningBudgetSlider?.default : undefined
if (reasoningBudget) {
return { type: 'budgetEnabled', isReasoningEnabled: isReasoningEnabled, reasoningBudget: reasoningBudget }
}
return null
}
@@ -0,0 +1,697 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../../base/common/uri.js';
import { os } from '../helpers/systemInfo.js';
import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { InternalToolInfo } from '../toolsServiceTypes.js';
// this is just for ease of readability
export const tripleTick = ['```', '```']
const changesExampleContent = `\
// ... existing code ...
// {{change 1}}
// ... existing code ...
// {{change 2}}
// ... existing code ...
// {{change 3}}
// ... existing code ...`
const editToolDescription = `\
${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const fileNameEdit = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
${tripleTick[1]}`
// ======================================================== tools ========================================================
const paginationHelper = {
desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`,
param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, }
} as const
export const voidTools = {
// --- context-gathering (read/search/list) ---
read_file: {
name: 'read_file',
description: `Returns file contents of a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
list_dir: {
name: 'list_dir',
description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`,
params: {
uri: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
pathname_search: {
name: 'pathname_search',
description: `Returns all pathnames that match a given \`find\`-style query (searches ONLY file names). You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
grep_search: {
name: 'grep_search',
description: `Returns all pathnames that match a given \`grep\`-style query (searches ONLY file contents). The query can be any regex. This is often followed by the \`read_file\` tool to view the full file contents of results. ${paginationHelper.desc}`,
params: {
query: { type: 'string', description: undefined },
...paginationHelper.param,
},
},
// --- editing (create/delete) ---
create_uri: {
name: 'create_uri',
description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`,
params: {
uri: { type: 'string', description: undefined },
},
},
delete_uri: {
name: 'delete_uri',
description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`,
params: {
uri: { type: 'string', description: undefined },
params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' }
},
},
edit: { // APPLY TOOL
name: 'edit',
description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`,
params: {
uri: { type: 'string', description: undefined },
changeDescription: {
type: 'string', description: `\
- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
- You must output your description in triple backticks.
Here's an example of a good description:\n${editToolDescription}.`
}
},
},
terminal_command: {
name: 'terminal_command',
description: `Executes a terminal command.`,
params: {
command: { type: 'string', description: 'The terminal command to execute.' },
waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` },
terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' },
},
},
// go_to_definition
// go_to_usages
} satisfies { [name: string]: InternalToolInfo }
// ======================================================== chat (normal, gather, agent) ========================================================
export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\
You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} that runs in the Void code editor. Your job is \
${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to their codebase. You should ALWAYS bring user's task to completion to the fullest extent possible, calling tools to make all necessary changes.`
: mode === 'gather' ? `to search and understand the user's codebase. You MUST use tools to read files and help the user understand the codebase, even if you were initially given files.`
: mode === 'normal' ? `to assist the user with their coding tasks.`
: ''}
You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`.
Please assist the user with their query. The user's query is never invalid.
${/* system info */''}
The user's system information is as follows:
- ${os}
- Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'}
${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\
- Existing terminal IDs: ${runningTerminalIds.join(', ')}
`: '\n'}
${/* tool use */ mode === 'agent' || mode === 'gather' ? `\
You will be given tools you can call.
${mode === 'agent' ? `\
- Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.
- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.
- You will OFTEN need to gather context before making a change. Do not immediately make a change unless you have ALL relevant context.
- ALWAYS have maximal certainty in a change BEFORE you make it. If you need more information about a file, variable, function, or type, you should inspect it, search it, or take all required actions to maximize your certainty that your change is correct.`
: mode === 'gather' ? `\
- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.
- You should extensively read files, types, etc and gather relevant context.`
: ''}
- If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc.
- NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results.
- Some tools only work if the user has a workspace open.${mode === 'agent' ? `
- NEVER modify a file outside the user's workspace(s) without permission from the user.` : ''}
\
`: `\
You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it.
\
`}
${/* code blocks */ mode === 'agent' ? `\
Behavior:
- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them.
- Prioritize taking as many steps as you need to complete your request over stopping early.\
`: `\
If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks).
- The first line of the code block must be the FULL PATH of the file you want to change.
- The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing.
- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible.
- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise.
Here's an example of a good code block:\n${fileNameEdit}.\
`}
${/* misc */''}
Misc:
- Do not make things up.
- Do not be lazy.
- NEVER re-write the entire file.
- Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\
`
// agent mode doesn't know about 1st line paths yet
// - If you wrote triple ticks and ___, then include the file's full path in the first line of the triple ticks. This is only for display purposes to the user, and it's preferred but optional. Never do this in a tool parameter, or if there's ambiguity about the full path.
type FileSelnLocal = { fileURI: URI, language: string, content: string }
const stringifyFileSelection = ({ fileURI, language, content }: FileSelnLocal) => {
return `\
${fileURI.fsPath}
${tripleTick[0]}${language}
${content}
${tripleTick[1]}
`
}
const stringifyCodeSelection = ({ fileURI, language, selectionStr, range }: CodeSelection) => {
return `\
${fileURI.fsPath} (lines ${range.startLineNumber}:${range.endLineNumber})
${tripleTick[0]}${language}
${selectionStr}
${tripleTick[1]}
`
}
const failToReadStr = 'Could not read content. This file may have been deleted. If you expected content here, you can tell the user about this as they might not know.'
const stringifyFileSelections = async (fileSelections: FileSelection[], voidModelService: IVoidModelService) => {
if (fileSelections.length === 0) return null
const fileSlns: FileSelnLocal[] = await Promise.all(fileSelections.map(async (sel) => {
const { model } = await voidModelService.getModelSafe(sel.fileURI)
const content = model?.getValue(EndOfLinePreference.LF) ?? failToReadStr
return { ...sel, content }
}))
return fileSlns.map(sel => stringifyFileSelection(sel)).join('\n')
}
const stringifyCodeSelections = (codeSelections: CodeSelection[]) => {
return codeSelections.map(sel => {
stringifyCodeSelection(sel)
}).join('\n') || null
}
const stringifySelectionNames = (currSelns: StagingSelectionItem[] | null): string => {
if (!currSelns) return ''
return currSelns.map(s => `${s.fileURI.fsPath}${s.range ? ` (lines ${s.range.startLineNumber}:${s.range.endLineNumber})` : ''}`).join('\n')
}
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null) => {
const selnsStr = stringifySelectionNames(currSelns)
let str = ''
if (selnsStr) { str += `SELECTIONS\n${selnsStr}\n` }
str += `\nINSTRUCTIONS\n${instructions}`
return str;
};
export const chat_selectionsString = async (
prevSelns: StagingSelectionItem[] | null, currSelns: StagingSelectionItem[] | null,
voidModelService: IVoidModelService,
) => {
// ADD IN FILES AT TOP
const allSelections = [...currSelns || [], ...prevSelns || []]
if (allSelections.length === 0) return null
const codeSelections: CodeSelection[] = []
const fileSelections: FileSelection[] = []
const filesURIs = new Set<string>()
for (const selection of allSelections) {
if (selection.type === 'Selection') {
codeSelections.push(selection)
}
else if (selection.type === 'File') {
const fileSelection = selection
const path = fileSelection.fileURI.fsPath
if (!filesURIs.has(path)) {
filesURIs.add(path)
fileSelections.push(fileSelection)
}
}
}
const filesStr = await stringifyFileSelections(fileSelections, voidModelService)
const selnsStr = stringifyCodeSelections(codeSelections)
const fileContents = [filesStr, selnsStr].filter(Boolean).join('\n')
return fileContents || null
}
export const chat_lastUserMessageWithFilesAdded = (userMessage: string, selectionsString: string | null) => {
if (userMessage) return `${userMessage}${selectionsString ? `\n${selectionsString}` : ''}`
else return userMessage
}
export const rewriteCode_systemMessage = `\
You are a coding assistant that re-writes an entire file to make a change. You are given the original file \`ORIGINAL_FILE\` and a change \`CHANGE\`.
Directions:
1. Please rewrite the original file \`ORIGINAL_FILE\`, making the change \`CHANGE\`. You must completely re-write the whole file.
2. Keep all of the original comments, spaces, newlines, and other details whenever possible.
3. ONLY output the full new file. Do not add any other explanations or text.
`
// ======================================================== apply (writeover) ========================================================
export const rewriteCode_userMessage = ({ originalCode, applyStr, language }: { originalCode: string, applyStr: string, language: string }) => {
return `\
ORIGINAL_FILE
${tripleTick[0]}${language}
${originalCode}
${tripleTick[1]}
CHANGE
${tripleTick[0]}
${applyStr}
${tripleTick[1]}
INSTRUCTIONS
Please finish writing the new file by applying the change to the original file. Return ONLY the completion of the file, without any explanation.
`
}
// ======================================================== apply (fast apply - search/replace) ========================================================
export const ORIGINAL = `<<<<<<< ORIGINAL`
export const DIVIDER = `=======`
export const FINAL = `>>>>>>> UPDATED`
export const searchReplace_systemMessage = `\
You are a coding assistant that generates SEARCH/REPLACE code blocks that will be used to edit a file.
A SEARCH/REPLACE block describes the code before and after a change. Here is the format:
${tripleTick[0]}
${ORIGINAL}
// ... original code goes here
${DIVIDER}
// ... final code goes here
${FINAL}
${tripleTick[1]}
You will be given the original file \`ORIGINAL_FILE\` and a description of a change \`CHANGE\` to make.
Output SEARCH/REPLACE blocks to edit the file according to the desired change. You may output multiple SEARCH/REPLACE blocks.
Directions:
1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this.
2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. This includes whitespace, comments, and other details.
3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file.
4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks.
The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY.
- Make sure you add all necessary imports.
- Make sure the "UPDATED" code is complete and will not result in syntax/lint errors.
Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise.
## EXAMPLE 1
ORIGINAL_FILE
${tripleTick[0]}
let w = 5
let x = 6
let y = 7
let z = 8
${tripleTick[1]}
CHANGE
Make x equal to 6.5, not 6.
${tripleTick[0]}
// ... existing code
let x = 6.5
// ... existing code
${tripleTick[1]}
## ACCEPTED OUTPUT
${tripleTick[0]}
${ORIGINAL}
let x = 6
${DIVIDER}
let x = 6.5
${FINAL}
${tripleTick[1]}
`
export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\
ORIGINAL_FILE
${originalCode}
CHANGE
${applyStr}`
export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullFileStr: string, startLine: number, endLine: number }) => {
const fullFileLines = fullFileStr.split('\n')
// we can optimize this later
const MAX_PREFIX_SUFFIX_CHARS = 20_000
/*
a
a
a <-- final i (prefix = a\na\n)
a
|b <-- startLine-1 (middle = b\nc\nd\n) <-- initial i (moves up)
c
d| <-- endLine-1 <-- initial j (moves down)
e
e <-- final j (suffix = e\ne\n)
e
e
*/
let prefix = ''
let i = startLine - 1 // 0-indexed exclusive
// we'll include fullFileLines[i...(startLine-1)-1].join('\n') in the prefix.
while (i !== 0) {
const newLine = fullFileLines[i - 1]
if (newLine.length + 1 + prefix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
prefix = `${newLine}\n${prefix}`
i -= 1
}
else break
}
let suffix = ''
let j = endLine - 1
while (j !== fullFileLines.length - 1) {
const newLine = fullFileLines[j + 1]
if (newLine.length + 1 + suffix.length <= MAX_PREFIX_SUFFIX_CHARS) { // +1 to include the \n
suffix = `${suffix}\n${newLine}`
j += 1
}
else break
}
return { prefix, suffix }
}
// ======================================================== quick edit (ctrl+K) ========================================================
export type QuickEditFimTagsType = {
preTag: string,
sufTag: string,
midTag: string
}
export const defaultQuickEditFimTags: QuickEditFimTagsType = {
preTag: 'ABOVE',
sufTag: 'BELOW',
midTag: 'SELECTION',
}
// this should probably be longer
export const ctrlKStream_systemMessage = ({ quickEditFIMTags: { preTag, midTag, sufTag } }: { quickEditFIMTags: QuickEditFimTagsType }) => {
return `\
You are a FIM (fill-in-the-middle) coding assistant. Your task is to fill in the middle SELECTION marked by <${midTag}> tags.
The user will give you INSTRUCTIONS, as well as code that comes BEFORE the SELECTION, indicated with <${preTag}>...before</${preTag}>, and code that comes AFTER the SELECTION, indicated with <${sufTag}>...after</${sufTag}>.
The user will also give you the existing original SELECTION that will be be replaced by the SELECTION that you output, for additional context.
Instructions:
1. Your OUTPUT should be a SINGLE PIECE OF CODE of the form <${midTag}>...new_code</${midTag}>. Do NOT output any text or explanations before or after this.
2. You may ONLY CHANGE the original SELECTION, and NOT the content in the <${preTag}>...</${preTag}> or <${sufTag}>...</${sufTag}> tags.
3. Make sure all brackets in the new selection are balanced the same as in the original selection.
4. Be careful not to duplicate or remove variables, comments, or other syntax by mistake.
`
}
export const ctrlKStream_userMessage = ({ selection, prefix, suffix, instructions, fimTags, isOllamaFIM, language }: {
selection: string, prefix: string, suffix: string, instructions: string, fimTags: QuickEditFimTagsType, language: string,
isOllamaFIM: false, // we require this be false for clarity
}) => {
const { preTag, sufTag, midTag } = fimTags
// prompt the model artifically on how to do FIM
// const preTag = 'BEFORE'
// const sufTag = 'AFTER'
// const midTag = 'SELECTION'
return `\
CURRENT SELECTION
${tripleTick[0]}${language}
<${midTag}>${selection}</${midTag}>
${tripleTick[1]}
INSTRUCTIONS
${instructions}
<${preTag}>${prefix}</${preTag}>
<${sufTag}>${suffix}</${sufTag}>
Return only the completion block of code (of the form ${tripleTick[0]}${language}
<${midTag}>...new code</${midTag}>
${tripleTick[1]}).`
};
/*
// ======================================================== ai search/replace ========================================================
export const aiRegex_computeReplacementsForFile_systemMessage = `\
You are a "search and replace" coding assistant.
You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE.
The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for.
The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace.
The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes.
## Instructions
1. If you do not want to make any changes, you should respond with the word "no".
2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make.
For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name.
- Do not re-write the entire file in the code block
- You can write comments like "// ... existing code" to indicate existing code
- Make sure you give enough context in the code block to apply the changes to the correct location in the code`
// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => {
// // we may want to do this in batches
// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } }
// const file = await stringifyFileSelections([fileSelection], voidFileService)
// return `\
// ## FILE
// ${file}
// ## SEARCH_CLAUSE
// Here is what the user is searching for:
// ${searchClause}
// ## REPLACE_CLAUSE
// Here is what the user wants to replace it with:
// ${replaceClause}
// ## INSTRUCTIONS
// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.`
// }
// // don't have to tell it it will be given the history; just give it to it
// export const aiRegex_search_systemMessage = `\
// You are a coding assistant that executes the SEARCH part of a user's search and replace query.
// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context.
// Output
// - Regex query
// - Files to Include (optional)
// - Files to Exclude? (optional)
// `
// ======================================================== old examples ========================================================
Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below.
## EXAMPLE 1
FILES
math.ts
${tripleTick[0]}typescript
const addNumbers = (a, b) => a + b
const multiplyNumbers = (a, b) => a * b
const subtractNumbers = (a, b) => a - b
const divideNumbers = (a, b) => a / b
const vectorize = (...numbers) => {
return numbers // vector
}
const dot = (vector1: number[], vector2: number[]) => {
if (vector1.length !== vector2.length) throw new Error(\`Could not dot vectors \${vector1} and \${vector2}. Size mismatch.\`)
let sum = 0
for (let i = 0; i < vector1.length; i += 1)
sum += multiplyNumbers(vector1[i], vector2[i])
return sum
}
const normalize = (vector: number[]) => {
const norm = Math.sqrt(dot(vector, vector))
for (let i = 0; i < vector.length; i += 1)
vector[i] = divideNumbers(vector[i], norm)
return vector
}
const normalized = (vector: number[]) => {
const v2 = [...vector] // clone vector
return normalize(v2)
}
${tripleTick[1]}
SELECTIONS
math.ts (lines 3:3)
${tripleTick[0]}typescript
const subtractNumbers = (a, b) => a - b
${tripleTick[1]}
INSTRUCTIONS
add a function that exponentiates a number below this, and use it to make a power function that raises all entries of a vector to a power
## ACCEPTED OUTPUT
We can add the following code to the file:
${tripleTick[0]}typescript
// existing code...
const subtractNumbers = (a, b) => a - b
const exponentiateNumbers = (a, b) => Math.pow(a, b)
const divideNumbers = (a, b) => a / b
// existing code...
const raiseAll = (vector: number[], power: number) => {
for (let i = 0; i < vector.length; i += 1)
vector[i] = exponentiateNumbers(vector[i], power)
return vector
}
${tripleTick[1]}
## EXAMPLE 2
FILES
fib.ts
${tripleTick[0]}typescript
const dfs = (root) => {
if (!root) return;
console.log(root.val);
dfs(root.left);
dfs(root.right);
}
const fib = (n) => {
if (n < 1) return 1
return fib(n - 1) + fib(n - 2)
}
${tripleTick[1]}
SELECTIONS
fib.ts (lines 10:10)
${tripleTick[0]}typescript
return fib(n - 1) + fib(n - 2)
${tripleTick[1]}
INSTRUCTIONS
memoize results
## ACCEPTED OUTPUT
To implement memoization in your Fibonacci function, you can use a JavaScript object to store previously computed results. This will help avoid redundant calculations and improve performance. Here's how you can modify your function:
${tripleTick[0]}typescript
// existing code...
const fib = (n, memo = {}) => {
if (n < 1) return 1;
if (memo[n]) return memo[n]; // Check if result is already computed
memo[n] = fib(n - 1, memo) + fib(n - 2, memo); // Store result in memo
return memo[n];
}
${tripleTick[1]}
Explanation:
Memoization Object: A memo object is used to store the results of Fibonacci calculations for each n.
Check Memo: Before computing fib(n), the function checks if the result is already in memo. If it is, it returns the stored result.
Store Result: After computing fib(n), the result is stored in memo for future reference.
## END EXAMPLES
*/
@@ -4,11 +4,11 @@
*--------------------------------------------------------------------------------------*/
import { IVoidSettingsService } from './voidSettingsService.js';
import { ILLMMessageService } from './llmMessageService.js';
import { ILLMMessageService } from './sendLLMMessageService.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, VLLMModelResponse } from './llmMessageTypes.js';
import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './llmMessageTypes.js';
import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './sendLLMMessageTypes.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
@@ -13,7 +13,6 @@ import { generateUuid } from '../../../../base/common/uuid.js';
import { Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js';
// import { INotificationService } from '../../notification/common/notification.js';
// calls channel to implement features
@@ -39,6 +38,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
onText: {} as { [eventId: string]: ((params: EventLLMMessageOnTextParams) => void) },
onFinalMessage: {} as { [eventId: string]: ((params: EventLLMMessageOnFinalMessageParams) => void) },
onError: {} as { [eventId: string]: ((params: EventLLMMessageOnErrorParams) => void) },
onAbort: {} as { [eventId: string]: (() => void) }, // NOT sent over the channel, result is instant when we call .abort()
}
// list hooks
@@ -67,13 +67,13 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
// 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')
this.channel = this.mainProcessService.getChannel('void-channel-llmMessage')
// .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead
// llm
this._register((this.channel.listen('onText_sendLLMMessage') satisfies Event<EventLLMMessageOnTextParams>)(e => { this.llmMessageHooks.onText[e.requestId]?.(e) }))
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._onRequestIdDone(e.requestId) }))
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._onRequestIdDone(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
this._register((this.channel.listen('onFinalMessage_sendLLMMessage') satisfies Event<EventLLMMessageOnFinalMessageParams>)(e => { this.llmMessageHooks.onFinalMessage[e.requestId]?.(e); this._clearChannelHooks(e.requestId) }))
this._register((this.channel.listen('onError_sendLLMMessage') satisfies Event<EventLLMMessageOnErrorParams>)(e => { this.llmMessageHooks.onError[e.requestId]?.(e); this._clearChannelHooks(e.requestId); console.error('Error in LLMMessageService:', JSON.stringify(e)) }))
// ollama .list()
this._register((this.channel.listen('onSuccess_list_ollama') satisfies Event<EventModelListOnSuccessParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.success[e.requestId]?.(e) }))
this._register((this.channel.listen('onError_list_ollama') satisfies Event<EventModelListOnErrorParams<OllamaModelResponse>>)(e => { this.listHooks.ollama.error[e.requestId]?.(e) }))
@@ -83,58 +83,48 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
}
sendLLMMessage(params: ServiceSendLLMMessageParams) {
const { onText, onFinalMessage, onError, ...proxyParams } = params;
const { useProviderFor: featureName } = proxyParams
const { onText, onFinalMessage, onError, onAbort, modelSelection, ...proxyParams } = params;
// throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend)
const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state)
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName]
if (isDisabled || modelSelection === null) {
let message: string
if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected')
message = `Please add a provider in Void's Settings.`
else if (isDisabled === 'addModel')
message = `Please add a model.`
else if (isDisabled === 'needToEnableModel')
message = `Please enable a model.`
else if (isDisabled === 'notFilledIn')
message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.`
else
message = `Please add a provider in Void's Settings.`
if (modelSelection === null) {
const message = `Please add a provider in Void's Settings.`
onError({ message, fullError: null })
return null
}
if (params.messagesType === 'chatMessages' && (params.messages?.length ?? 0) === 0) {
const message = `No messages detected.`
onError({ message, fullError: null })
return null
}
const { providerName, modelName } = modelSelection
// add state for request id
const requestId = generateUuid();
this.llmMessageHooks.onText[requestId] = onText
this.llmMessageHooks.onFinalMessage[requestId] = onFinalMessage
this.llmMessageHooks.onError[requestId] = onError
this.llmMessageHooks.onAbort[requestId] = onAbort // used internally only
const { aiInstructions } = this.voidSettingsService.state.globalSettings
const { settingsOfProvider } = this.voidSettingsService.state
const { settingsOfProvider, } = this.voidSettingsService.state
// params will be stripped of all its functions over the IPC channel
this.channel.call('sendLLMMessage', {
...proxyParams,
aiInstructions,
requestId,
providerName,
modelName,
settingsOfProvider,
modelSelection,
} satisfies MainSendLLMMessageParams);
return requestId
}
abort(requestId: string) {
this.llmMessageHooks.onAbort[requestId]?.() // calling the abort hook here is instant (doesn't go over a channel)
this.channel.call('abort', { requestId } satisfies MainLLMMessageAbortParams);
this._onRequestIdDone(requestId)
this._clearChannelHooks(requestId)
}
@@ -175,7 +165,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
} satisfies MainModelListParams<VLLMModelResponse>)
}
_onRequestIdDone(requestId: string) {
_clearChannelHooks(requestId: string) {
delete this.llmMessageHooks.onText[requestId]
delete this.llmMessageHooks.onFinalMessage[requestId]
delete this.llmMessageHooks.onError[requestId]

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