Compare commits

..

532 Commits

Author SHA1 Message Date
Andrew Pareles b3166e7ef2 all repos 2026-06-02 15:03:37 -07:00
Andrew Pareles 17e7a5b152 Update README.md 2026-01-12 01:57:57 -08:00
Andrew Pareles f100db9175 Update README.md 2026-01-12 01:25:52 -08:00
Andrew Pareles 1ee8980fe1 typo 2025-08-06 17:07:32 -07:00
Andrew Pareles 74650cb867 clearer instructions on developer mode and building locally 2025-08-06 17:04:13 -07:00
Andrew Pareles 5aaa1604e1 Merge pull request #858 from chrisnumbers/main
Updated code selection to properly parse selected lines
2025-08-04 15:22:47 -07:00
Andrew Pareles ff717cad16 update lines 2025-08-04 15:21:42 -07:00
Chris 7a974f2dff Fixed selection output to include triple backticks and language tag 2025-08-04 14:23:35 -04:00
Chris cbb52dec28 Removed unnecessary ternary in lineNumAdd 2025-08-04 10:55:59 -04:00
Chris d948fc5950 Updated code selection to properly parse selected lines 2025-08-03 19:06:52 -04:00
Andrew Pareles ee39ee94b1 add dev note 2025-08-01 02:06:28 -07:00
Andrew Pareles a7eb54a09b Update releases and add download button 2025-07-22 01:53:37 -07:00
Andrew Pareles 34e3413dca wiki token 2025-07-05 01:01:07 -07:00
Andrew Pareles 607a61c068 Merge pull request #797 from voideditor/revert-784-fix-ime-composition-chat
Revert "Fix: IME composition preventing message sending in chat"
2025-06-29 16:34:44 -07:00
Andrew Pareles 9732f74ba7 remove random as any casts 2025-06-29 16:34:35 -07:00
Andrew Pareles 9031215001 Revert "Fix: IME composition preventing message sending in chat" 2025-06-29 16:33:29 -07:00
Andrew Pareles 3fa95be625 Merge pull request #784 from tsujihidetaka/fix-ime-composition-chat
Fix: IME composition preventing message sending in chat
2025-06-29 16:31:38 -07:00
Andrew Pareles 1183cd3ad4 Merge pull request #697 from vrtnis/cron-issue-wiki
Add GitHub Action to triage issues and publish to wiki
2025-06-29 16:30:32 -07:00
Andrew Pareles ac1aba825f Merge pull request #747 from jcommaret/mistral-magistral
Mistral magistral
2025-06-29 15:51:50 -07:00
Andrew Pareles 867df38364 Merge pull request #791 from servetgulnaroglu/tooltip-for-selected-files
added tooltip for selected files the prevent ambiguity
2025-06-28 22:12:17 -07:00
Andrew Pareles c90f707070 add delay 2025-06-28 22:12:08 -07:00
Andrew Pareles 0424e7d0d3 Merge pull request #783 from cabelo/patch-1
SUSE and openSUSE instructions
2025-06-28 22:07:55 -07:00
servetgulnaroglu 42c50413f5 added tooltip for selected files the prevent ambiguity 2025-06-28 15:08:40 +03:00
Hidetaka Tsuji b5ae6d7a96 Fix: IME composition preventing message sending in chat 2025-06-24 13:54:51 +02:00
Alessandro de Oliveira Faria (A.K.A.CABELO) 453f392f01 SUSE and openSUSE instructions 2025-06-24 02:22:01 -03:00
Andrew Pareles b5a41840a0 Merge pull request #775 from voideditor/mcp
clearer wording
2025-06-23 01:05:25 -07:00
Andrew Pareles f0ad8e2e2e 1.4.9 2025-06-23 04:04:55 -04:00
Andrew Pareles d732d6ba05 re-add msft telemetry option but with clearer distinction of void vs msft 2025-06-23 04:04:28 -04:00
Andrew Pareles 24add9286c Merge pull request #774 from voideditor/mcp
Mcp
2025-06-22 22:20:56 -07:00
Andrew Pareles 67e89fd979 bump 2025-06-23 01:20:34 -04:00
Andrew Pareles 5a702f98bc remove leftover vscode telemetry setting; replaced with Void Settings option 2025-06-23 01:20:05 -04:00
Andrew Pareles 8b3b118319 Merge pull request #773 from voideditor/mcp
lint fix
2025-06-22 21:37:18 -07:00
Andrew Pareles c3125804f3 lint fix 2025-06-23 00:36:58 -04:00
Andrew Pareles b739e99a1e Merge pull request #772 from voideditor/mcp
fix
2025-06-22 21:26:34 -07:00
Andrew Pareles ec3b7caa8e fix 2025-06-23 00:26:09 -04:00
Andrew Pareles ffdbb214f9 Merge pull request #771 from voideditor/mcp
disabled fix + bump
2025-06-22 21:19:08 -07:00
Andrew Pareles ec5b7636be disabled fix + bump 2025-06-23 00:18:44 -04:00
Andrew Pareles d1adb8b0fc Merge pull request #768 from voideditor/mcp
bump 1.4.5
2025-06-21 14:57:08 -07:00
Andrew Pareles a475c80f4f bump 1.4.5 2025-06-21 14:56:44 -07:00
Andrew Pareles 8233c59f2b Merge pull request #767 from voideditor/mcp
Metrics toggle
2025-06-21 14:56:23 -07:00
Andrew Pareles 7c2192a7dc message 2025-06-21 14:54:35 -07:00
Andrew Pareles 6aa2409302 update 2025-06-21 14:42:36 -07:00
Mathew Pareles b48c3aaf35 proper ordering 2025-06-21 04:12:52 -07:00
Mathew Pareles 722d0c5b3b settings styles 2025-06-21 04:08:16 -07:00
Mathew Pareles 7727b19b3c finish adding toggle 2025-06-21 04:07:23 -07:00
Mathew Pareles 0816694210 clearer wording 2025-06-21 03:55:01 -07:00
Mathew Pareles c6d210902b start adding toggle metrics 2025-06-21 03:50:08 -07:00
Andrew Pareles b00d77610d Merge pull request #761 from voideditor/mcp
add icons
2025-06-19 13:58:56 -07:00
Andrew Pareles ad8fedc307 add icons 2025-06-19 13:58:41 -07:00
Jérôme Commaret 940155ac80 Added reasoning parameters 2025-06-12 19:46:30 +02:00
Jérôme Commaret ffbfba8d4f [Feature] Add magistral-small-2506 #741 2025-06-12 19:43:01 +02:00
Andrew Pareles da425ab0fe Merge pull request #738 from voideditor/mcp
1.4.4
2025-06-09 17:31:25 -07:00
Andrew Pareles 1191abf745 1.4.4 2025-06-09 17:31:01 -07:00
Andrew Pareles 1a922133da Merge pull request #737 from voideditor/mcp
mcp tool interrupt fix
2025-06-09 17:30:21 -07:00
Andrew Pareles 40a27cd1ee mcp tool interrupt fix 2025-06-09 17:30:03 -07:00
Andrew Pareles bf062146c9 Merge pull request #720 from voideditor/mcp
fix?
2025-06-06 01:44:50 -07:00
Andrew Pareles 97cb63c7fc fix? 2025-06-06 01:42:44 -07:00
Andrew Pareles 2f283b503a Merge pull request #719 from voideditor/mcp
temp fixes
2025-06-06 01:37:06 -07:00
Andrew Pareles 7b78f962d3 rm scm generation until we can dismiss the errors 2025-06-06 01:35:54 -07:00
Andrew Pareles 93ee1731b5 remove type to try to fix git errors 2025-06-06 01:15:04 -07:00
Andrew Pareles f5680a5c62 Merge pull request #718 from voideditor/mcp
add to history on autoaccept
2025-06-06 01:10:27 -07:00
Andrew Pareles 88288dba46 add to history on autoaccept 2025-06-06 01:09:35 -07:00
Andrew Pareles 5b7580c3da Merge pull request #717 from voideditor/mcp
bump 1.4.3
2025-06-06 01:01:15 -07:00
Andrew Pareles 9bf0646abe bump 1.4.3 2025-06-06 01:00:30 -07:00
Andrew Pareles 1737c7bfc8 Merge pull request #716 from voideditor/mcp
undo validateURI change
2025-06-06 00:56:48 -07:00
Andrew Pareles 584dc18ede undo validateURI change 2025-06-06 00:56:22 -07:00
Andrew Pareles 3ec6973a0f Merge pull request #714 from voideditor/mcp
add auto accept toggle
2025-06-06 00:28:13 -07:00
Andrew Pareles 42ef028c1e add auto accept toggle 2025-06-06 00:27:47 -07:00
Andrew Pareles 7b14f0ca73 Merge pull request #558 from steaks/commit-prompt
Added Generate Commit Message button for git
2025-06-05 19:28:41 -07:00
Andrew Pareles 9a5580d32e sync SCM 2025-06-05 16:14:40 -07:00
Andrew Pareles c41838afc6 git commit 2025-06-05 16:06:13 -07:00
Andrew Pareles f32743d759 Merge pull request #711 from Sherlock-Holo/reasoning_content
openai compatible support reasoning_content field
2025-06-05 15:52:15 -07:00
Steven Wexler 9e53dfef33 Support staged vs. unstaged changs. Increase diff size to 8000. 2025-06-05 09:56:07 -04:00
Steven Wexler 844784445c Localize commit message generation error notification 2025-06-05 09:35:26 -04:00
Steven Wexler 9920bd3366 Merge remote-tracking branch 'upstream/main' into commit-prompt 2025-06-05 09:33:28 -04:00
Sherlock Holo 483cfb29a0 openai compatible support reasoning_content field 2025-06-05 19:12:21 +08:00
Andrew Pareles 282800e394 Merge pull request #710 from voideditor/mcp
ssh/wsl fix + ui
2025-06-05 02:47:28 -07:00
Andrew Pareles aacae4728f 1.4.2 version bump 2025-06-05 02:47:07 -07:00
Andrew Pareles 77c9284df5 fix ssh? 2025-06-05 02:41:58 -07:00
Andrew Pareles 7eac4aad6f remove ugly button 2025-06-05 02:41:54 -07:00
vrtnis 797c8e7096 Add GitHub Action to triage issues and publish to wiki 2025-06-02 00:18:58 +00:00
Andrew Pareles 406d0dc297 Merge pull request #692 from voideditor/mcp
1.4.1
2025-05-30 23:40:54 -07:00
Andrew Pareles e34e38cb3a 1.4.1 2025-05-30 23:40:34 -07:00
Andrew Pareles 95f2aa70a1 Merge pull request #691 from voideditor/mcp
fix terminal cmdCap issues
2025-05-30 23:40:17 -07:00
Andrew Pareles b74772e6e2 cmdCap 2025-05-30 23:40:02 -07:00
Andrew Pareles 374e6c7109 Merge pull request #690 from voideditor/mcp
1.4.0 - MCP!
2025-05-30 21:16:08 -07:00
Andrew Pareles 8e6bb7194d 1.4.0! 2025-05-30 21:15:24 -07:00
Andrew Pareles 2b4283a0f0 rm comments 2025-05-30 21:10:23 -07:00
Andrew Pareles b0617700aa Merge branch 'main' into pr/steaks/558 2025-05-30 21:04:50 -07:00
Andrew Pareles 53f7aa47f6 Merge pull request #689 from voideditor/mcp
Mcp error codes
2025-05-30 21:03:02 -07:00
Andrew Pareles e324257e1a Merge remote-tracking branch 'origin/main' into mcp 2025-05-30 21:02:52 -07:00
Andrew Pareles 7feb9154da err codes 2025-05-30 21:02:21 -07:00
Andrew Pareles 857eda72fd remove toolname prefix in sidebar and remove unused logic 2025-05-30 20:54:52 -07:00
Andrew Pareles 3bc0a34e83 Merge pull request #688 from voideditor/test1
update voidrules
2025-05-30 20:40:12 -07:00
Andrew Pareles 9c642763f0 update voidrules 2025-05-30 20:39:57 -07:00
Andrew Pareles 17e44e192d Merge pull request #686 from bjoaquinc/mcp-debugging
fix: mcp tool call and tool call error response
2025-05-30 20:38:11 -07:00
Joaquin Coromina a24d097264 add remove prefix function for tool call, updated the tool call error to return tool call error response string rather than content object 2025-05-30 17:43:06 -04:00
Andrew Pareles 2880549d3f merge properly 2025-05-30 03:40:14 -07:00
Andrew Pareles 22fa093928 sync 2025-05-30 01:26:34 -07:00
Andrew Pareles 31950ae01e sync scm 2025-05-30 01:24:14 -07:00
Andrew Pareles 12a1014534 Merge branch 'main' into pr/steaks/558 2025-05-30 01:17:52 -07:00
Andrew Pareles 5815c0180a Merge pull request #654 from vrtnis/void-settings-sidebar
feat(settings): add “All Settings” tab + smooth-scroll navigation
2025-05-30 01:13:25 -07:00
Andrew Pareles 822430535e MCP 2025-05-30 01:12:34 -07:00
Andrew Pareles 020fb60d74 mcp tool rename 2025-05-30 01:08:11 -07:00
Andrew Pareles 615f8c3315 finish merge settings 2025-05-30 01:06:38 -07:00
Andrew Pareles 980d7937c3 Merge branch 'main' into pr/vrtnis/654 2025-05-30 01:05:24 -07:00
Andrew Pareles 57d94d2d44 fix spacing issues and make scroll whole settings page 2025-05-30 00:54:59 -07:00
Andrew Pareles 382ecaae1b auto format 2025-05-30 00:34:42 -07:00
Andrew Pareles a9abdf6351 Merge pull request #623 from vrtnis/aws-bedrock
feat(void): add Bedrock provider in onboarding + type updates
2025-05-30 00:28:40 -07:00
Andrew Pareles 27c40d8428 key 2025-05-30 00:28:22 -07:00
Andrew Pareles c067d9fbf0 Merge pull request #684 from voideditor/andrew/fix-mcp-styling-consistency
sys message ui
2025-05-30 00:23:57 -07:00
Andrew Pareles 83c4a05642 toggle 2025-05-30 00:15:39 -07:00
Andrew Pareles 182af95035 Merge pull request #661 from animeshlego5/feat/disable-ai-system-message
feat: Add Toggle to Disable AI System Message Context
2025-05-29 23:58:34 -07:00
Andrew Pareles 0a6be740e1 simplify 2025-05-29 23:58:21 -07:00
Andrew Pareles 8d0cdb8d36 Merge remote-tracking branch 'origin/main' into pr/animeshlego5/661-1 2025-05-29 23:57:55 -07:00
Andrew Pareles 29f004a826 sys 2025-05-29 23:56:55 -07:00
Andrew Pareles ac6b5601ef remove optional variables that arent even used 2025-05-29 22:45:11 -07:00
Andrew Pareles cfb15e1117 remove unused variable 2025-05-29 22:41:21 -07:00
Andrew Pareles bd13208513 rm todo 2025-05-29 22:37:59 -07:00
Andrew Pareles 8350fbd2d1 Include both staged and unstaged changes in git diff statistics 2025-05-29 22:36:36 -07:00
Andrew Pareles 22e08e6a06 rename 2025-05-29 22:32:44 -07:00
Andrew Pareles b50079709d rename scm to match void convention 2025-05-29 22:32:11 -07:00
Andrew Pareles e2863d3002 simplify 2025-05-29 22:20:16 -07:00
Andrew Pareles c12e9ca9f8 Merge pull request #630 from voideditor/mcp
bjoaquinc/mcp - MCP Implementation
2025-05-29 19:16:24 -07:00
Andrew Pareles e138744816 updates 2025-05-29 19:12:34 -07:00
Andrew Pareles df21a99ed9 ignore scroll 2025-05-29 18:48:37 -07:00
Andrew Pareles 98e07e1c7f misc ui 2025-05-29 17:23:55 -07:00
Andrew Pareles 77e562e278 misc improvements 2025-05-29 16:31:40 -07:00
Andrew Pareles 232264d5fe + 2025-05-29 03:07:44 -07:00
Andrew Pareles 70d484049c rm any casts 2025-05-29 03:07:34 -07:00
Andrew Pareles f4557ef6a8 opus 4 edits:
----
1. parse id code  { "code": -32602, "name": "McpError" } out and display it to the user
2. deleting an mcp server from the mcp file doesnt work, the server remains
3. ui - non-wrapping tooltips should wrap
2025-05-29 03:03:12 -07:00
Andrew Pareles 3038b420c9 Merge remote-tracking branch 'origin/main' into pr/animeshlego5/661-1 2025-05-29 00:22:02 -07:00
Joaquin Coromina 0625ce4b40 Updated tool name to add a unique prefix on a per session basis. Created a function that removes that prefix when displaying tools to users 2025-05-27 16:09:50 -04:00
Steven Wexler 06d20d705e Remove log 2025-05-26 18:07:40 -04:00
Steven Wexler f034be08c3 Refactor: Update commit message generation to use new prompt structure and system messages 2025-05-26 13:04:41 -04:00
Steven Wexler f74b94e3ba Refactor: Move and rename prompt generation logic for commit messages 2025-05-26 13:04:23 -04:00
Steven Wexler d09601191d Merge remote-tracking branch 'upstream/main' into commit-prompt 2025-05-26 11:44:11 -04:00
Steven Wexler 189e207499 Improve system message prompt 2025-05-26 11:41:45 -04:00
Steven Wexler f88dba7610 Add git branch and log info to prompt 2025-05-26 11:41:12 -04:00
Andrew Pareles 44f56b3ab4 Merge pull request #653 from vdawger/chore-update-model-capabilities-grok-mini
chore: add grok mini and fast models to dropdown options
2025-05-26 01:31:39 -07:00
Steven Wexler 5e100fcf1d Limit commit message length 2025-05-25 22:01:41 -04:00
Steven Wexler 9e745e2b87 Merge remote-tracking branch 'upstream/main' into commit-prompt 2025-05-25 14:19:40 -04:00
Steven Wexler 3a39551205 Handle loading, aborting, debouncing, and errors 2025-05-25 09:18:08 -04:00
Steven Wexler 033a23aa28 Update service name 2025-05-25 09:17:01 -04:00
Steven Wexler 38f06202ed Sync scm model to chat model 2025-05-25 09:16:46 -04:00
Steven Wexler 327d2dfe06 Fix undefined files and propagate git errors 2025-05-25 09:16:19 -04:00
Andrew Pareles 826571b410 remove ToolName from extractGrammar 2025-05-24 17:09:35 -07:00
Animesh Gosain c185807e60 feat: Add toggle to disable AI system message
Refactor role mapping logic

role

Fix keymap for Void: Quick Edit to resolve conflict

The current keymap causes a conflict between 'Void: Quick Edit' and 'Clear Console'/'Clear Terminal'. Using this _when_ expression, we can resolve the conflict without affecting functionality.

* Add the when expression `editorFocus && !terminalFocus` to the keybinding for 'Void: Quick Edit' `void.ctrlKAction`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/voideditor/void?shareId=XXXX-XXXX-XXXX-XXXX).

feat: add Devstral model mapping + fix quick-edit keybinding type

claude 4

feat: Add <file_name_or_pattern> to .gitignore

remove whitespace

changed gitignore

changed gitignore again

changed gitignore again
2025-05-24 19:39:14 +05:30
Animesh Gosain 31cde552a0 Local Changes 2025-05-24 19:39:14 +05:30
vrtnis 23bb28f3c0 Merge branch 'main' into void-settings-sidebar 2025-05-23 11:27:39 -07:00
William Viana 5a34feb328 chore: add grok mini and fast models to dropdown options 2025-05-23 12:16:01 -05:00
Andrew Pareles f0f536bc2d mcp ui 2025-05-23 01:45:21 -07:00
Andrew Pareles 09f5f1fad1 better result display 2025-05-22 17:22:07 -07:00
Andrew Pareles 31aa31a18b call 2025-05-22 17:06:29 -07:00
Andrew Pareles b56cc0d662 mcp errors handling 2025-05-22 17:04:52 -07:00
Andrew Pareles 4f4af9b8b9 ui 2025-05-22 16:58:32 -07:00
Andrew Pareles 66de84f4e3 auto approve 2025-05-22 16:46:52 -07:00
Andrew Pareles 0a02342128 MCP UI 2025-05-22 16:42:04 -07:00
Joaquin Coromina f99589aaa6 Fixed isOn server name setting 2025-05-22 19:41:25 -04:00
Andrew Pareles da6ffecc1d mcp works! 2025-05-22 15:31:56 -07:00
Andrew Pareles 43b6dba7a3 tool progress 2025-05-22 14:47:56 -07:00
Joaquin Coromina 5747904b85 Created getMCPTools function which transforms the MCP tool to a similar shape to voidTools, also created a helper functions which turns InputSchema into params 2025-05-22 15:43:43 -04:00
Andrew Pareles 4358b9e384 Merge pull request #647 from voideditor/model-selection
Claude 4
2025-05-22 11:23:04 -07:00
Andrew Pareles 5a5732601e claude 4 2025-05-22 11:22:36 -07:00
Andrew Pareles 83ec200039 claude 4 2025-05-22 11:22:06 -07:00
Andrew Pareles 81ab21868a mcp ui + misc 2025-05-22 02:50:26 -07:00
Andrew Pareles 627ec18cc9 got event fires 2025-05-22 02:30:43 -07:00
Andrew Pareles 883a4663c9 clarify 2025-05-22 02:21:17 -07:00
Andrew Pareles d2d47175b5 cleanup + final mcp todos 2025-05-22 02:18:14 -07:00
Andrew Pareles 7aacbca580 updates/fixes to mcp 2025-05-22 02:04:28 -07:00
Andrew Pareles f5b0f24445 fix 2025-05-21 23:51:00 -07:00
Andrew Pareles 89ac548ae9 clearer types 2025-05-21 23:29:41 -07:00
Andrew Pareles 57bc32ac9b update loading? 2025-05-21 22:51:55 -07:00
Andrew Pareles 446248aa79 loading 2025-05-21 22:02:24 -07:00
Andrew Pareles 66575b5381 add bg colors 2025-05-21 21:38:30 -07:00
Andrew Pareles 8b99b6ec78 userspecified 2025-05-21 21:33:05 -07:00
Andrew Pareles 18d1f2e6b9 move MCP to settings 2025-05-21 21:24:46 -07:00
Andrew Pareles 72143608a1 misc fixes 2025-05-21 20:57:54 -07:00
Andrew Pareles ec2ea70395 Merge pull request #638 from vrtnis/add-devstral
feat: add devstral model mapping + fix quick-edit keybinding type
2025-05-21 19:48:41 -07:00
Andrew Pareles 55691624dd rename 2025-05-21 19:47:47 -07:00
Andrew Pareles 9072668be3 Merge remote-tracking branch 'upstream/mcp' into mcp 2025-05-21 19:46:49 -07:00
vrtnis 7a4cf6c955 Merge branch 'main' into add-devstral 2025-05-21 19:26:56 -07:00
vrtnis 588953be96 feat: add Devstral model mapping + fix quick-edit keybinding type 2025-05-22 02:01:51 +00:00
Andrew Pareles 5bdbbc84aa Merge pull request #636 from voideditor/revert-633-keymap-ctrlK
Revert "Fix keymap for Void: Quick Edit to resolve conflict"
2025-05-21 18:25:51 -07:00
Andrew Pareles ebae55cd4b Revert "Fix keymap for Void: Quick Edit to resolve conflict" 2025-05-21 18:25:23 -07:00
Andrew Pareles 2b9e2088b3 Merge pull request #633 from anschmieg/keymap-ctrlK
Fix keymap for Void: Quick Edit to resolve conflict
2025-05-21 14:01:03 -07:00
anschmieg cbc7cf0841 Fix keymap for Void: Quick Edit to resolve conflict
The current keymap causes a conflict between 'Void: Quick Edit' and 'Clear Console'/'Clear Terminal'. Using this _when_ expression, we can resolve the conflict without affecting functionality. 

* Add the when expression `editorFocus && !terminalFocus` to the keybinding for 'Void: Quick Edit' `void.ctrlKAction`.

---

For more details, open the [Copilot Workspace session](https://copilot-workspace.githubnext.com/voideditor/void?shareId=XXXX-XXXX-XXXX-XXXX).
2025-05-21 17:58:34 +02:00
Andrew Pareles d141bffae9 misc 2025-05-21 02:02:12 -07:00
Andrew Pareles 3619a3e671 add loading 2025-05-21 01:48:37 -07:00
Andrew Pareles 13654389c6 update state 2025-05-21 01:38:15 -07:00
Andrew Pareles 4d8363026e progress 2025-05-21 00:56:14 -07:00
Andrew Pareles d80fd766f8 autoformat 2025-05-20 23:18:52 -07:00
Andrew Pareles 6d9027f63b misc 2025-05-20 23:18:46 -07:00
Andrew Pareles 03ab6559fc misc 2025-05-20 23:09:23 -07:00
Joaquin Coromina 35f23261b0 Fixed the string definition for the mcp resultToString fns 2025-05-20 12:44:29 -04:00
Joaquin Coromina 9b4c0b8d5a Added functions that return properly formatted CallTool functions and ToolResultToStr functions from MCP tools, then hooked it up 2025-05-19 22:38:56 -04:00
Joaquin Coromina 07df20b4de Added mcpServerName to InternalToolInfo 2025-05-19 22:37:26 -04:00
vrtnis 5995591296 feat(void): add Bedrock provider in onboarding + type updates 2025-05-20 01:36:16 +00:00
Joaquin Coromina 6646bd0993 Added function in mcpService to handle calling an MCP server tool 2025-05-19 16:52:43 -04:00
Joaquin Coromina f1c9e29f61 Updated types and hooked up return value to callTool and safeCallTool functions 2025-05-19 16:16:48 -04:00
Joaquin Coromina 2b97326eff Added types to MCP tool call response 2025-05-19 16:00:30 -04:00
Joaquin Coromina b70ee20b88 Added an error to handle unsupported response types 2025-05-19 16:00:07 -04:00
Joaquin Coromina ae0068ecb3 Added callTool function to handle server tool calls 2025-05-19 15:01:56 -04:00
Andrew Pareles 091bbf4dd3 Merge pull request #608 from animeshlego5/fix-system-role
[Fix] system role for o-series models
2025-05-19 01:34:51 -07:00
Andrew Pareles 7642b3281a role 2025-05-19 01:34:29 -07:00
Animesh Gosain 05e27a0885 Refactor role mapping logic 2025-05-19 13:34:01 +05:30
vrtnis 1d15d29a19 Updated Settings.tsx with new sidebar tabs 2025-05-18 20:46:45 +00:00
Joaquin Coromina 807086f7b6 Added persistence between sessions for mcpServerState 2025-05-18 04:05:41 -04:00
Joaquin Coromina 2d910835b7 Updated the void settings to include mcpServerState 2025-05-18 04:04:49 -04:00
Joaquin Coromina b68d8fd8e8 Added toggle service to turn mcp servers on and off 2025-05-17 19:23:14 -04:00
Joaquin Coromina 59dd7d7eea Added proper error handling for config file parsing errors 2025-05-17 18:21:55 -04:00
Joaquin Coromina 079ce49d17 Fixed bug where isUpdated is never returned 2025-05-17 18:21:01 -04:00
Joaquin Coromina 7f4470674d Updated the client side to use new events and mcpService 2025-05-17 17:42:28 -04:00
Joaquin Coromina b1b7afe8a0 Updated service types to match new events and organized it better 2025-05-17 17:41:46 -04:00
Joaquin Coromina d3757e81b4 Changed mcpConfigService to mcpService and updated events to match channel 2025-05-17 17:41:10 -04:00
Joaquin Coromina 065470afc1 Updated the channel it include events for add, update, delete, and loading. Fixed a bug related to client.close() throwing and error 2025-05-17 17:36:37 -04:00
Animesh Gosain db771c4f33 Fix system role for o-series models 2025-05-17 16:24:13 +05:30
Joaquin Coromina 3c96193b71 Added an event for reinitializing all servers when config file is updated 2025-05-17 00:03:44 -04:00
Joaquin Coromina c669a987c5 Removed the custom toggle button and added MCPServersList to the main Settings 2025-05-16 23:28:06 -04:00
Joaquin Coromina 543c6f0550 Sent back command formatted for display 2025-05-16 23:25:31 -04:00
Joaquin Coromina 8afaec30da Updated the types to include commmand for display 2025-05-16 23:25:01 -04:00
Joaquin Coromina a0f9309e02 Updated the service to include a service that updates the browser whenever an event from the main-process comes in 2025-05-16 23:24:29 -04:00
Joaquin Coromina 243349a90f Created a server list UI and hooked it up to Config Service 2025-05-16 23:23:35 -04:00
Joaquin Coromina 04a821b382 Merge branch 'main' into mcp 2025-05-16 17:26:24 -04:00
Andrew Pareles 906502f660 Merge pull request #590 from voideditor/model-selection
Auto update message + terminal tool service improvements on Windows
2025-05-16 13:29:57 -07:00
Joaquin Coromina 2817cdd8a6 Created function for getting all tools from mcp servers formatted for void 2025-05-16 00:38:09 -04:00
Andrew Pareles a8bc42b524 Merge pull request #595 from zpg6/fix/azure-model-selection
fix: select AzureOpenAI deployment based on selected model
2025-05-15 21:01:32 -07:00
Joaquin Coromina 05e1e61f08 Added error channel event listener and a utility function to handle all server events 2025-05-15 22:47:24 -04:00
Joaquin Coromina 30655678fe Connected this service to the mcpChannel for interaction with the servers 2025-05-15 22:30:15 -04:00
Joaquin Coromina ca51bcaecc Added @modelcontextprotocol/sdk to the allowed list of imports to avoid typescript error 2025-05-15 22:29:22 -04:00
Joaquin Coromina a629736683 Registered mcpChannel 2025-05-15 22:28:48 -04:00
Joaquin Coromina 0074189191 Removed mcpService from common because it was supposed to handle setting up the servers but can't because of a lack of access to node dependencies 2025-05-15 22:28:14 -04:00
Joaquin Coromina 4a862641cd Added a types file to handle all MCP Server interfaces and response values 2025-05-15 22:27:05 -04:00
Joaquin Coromina b18ac4b6c0 Created mcpChannel to handle all mcp server setup and events in electron-main 2025-05-15 22:26:11 -04:00
Zach Grimaldi d7201a38dc fix: select AzureOpenAI deployment based on selected model 2025-05-15 11:34:56 -04:00
Andrew Pareles 9112c0c94d only 1 void update notif at a time 2025-05-14 15:11:05 -07:00
Joaquin Coromina 716cb8d591 Replaced @modelcontextprotocol/inspector-cli for @modelcontextprotocol/sdk 2025-05-14 11:41:22 -04:00
Joaquin Coromina aed9f31185 Added initial scaffolding 2025-05-14 11:23:35 -04:00
Joaquin Coromina 2377c18241 Added @modelcontextprotocol/inspector-cli for MCP client interactions and server interactions 2025-05-14 11:23:02 -04:00
Joaquin Coromina 7b9e5ce517 Created mcpService file to handle MCP commands 2025-05-14 11:08:24 -04:00
Joaquin Coromina c119286571 Merge branch 'voideditor:main' into mcp 2025-05-14 11:05:51 -04:00
Andrew Pareles 5cab7e61e5 1.3.10 2025-05-13 23:37:48 -07:00
Andrew Pareles c405a462a8 Merge pull request #584 from voideditor/model-selection
add reasoning low/med/hi for all openai-compat providers
2025-05-13 23:37:34 -07:00
Andrew Pareles e481327327 add reasoning low/med/hi for all openai-compat providers 2025-05-13 23:35:19 -07:00
Andrew Pareles b7bb7f2f94 Merge pull request #583 from voideditor/model-selection
1.3.9
2025-05-13 22:59:47 -07:00
Andrew Pareles 2ae288e2ed 1.3.9 2025-05-13 22:59:28 -07:00
Andrew Pareles 316709671f Merge pull request #582 from voideditor/model-selection
fix certain cases of advanced settings
2025-05-13 22:59:16 -07:00
Andrew Pareles 9fac1e27a9 fix certain cases of advanced settings 2025-05-13 22:58:37 -07:00
Andrew Pareles f632327a7e Merge pull request #580 from voideditor/model-selection
1.3.8
2025-05-13 21:33:26 -07:00
Andrew Pareles e95922802f rm azure identity 2025-05-13 21:32:57 -07:00
Andrew Pareles 3954056a41 1.3.8 2025-05-13 21:32:21 -07:00
Andrew Pareles 1c7acdac94 Merge remote-tracking branch 'origin/main' into model-selection 2025-05-13 21:31:39 -07:00
Andrew Pareles 33163b7e81 Merge branch 'main' into model-selection 2025-05-13 21:31:09 -07:00
Andrew Pareles d9d3147b2a Merge pull request #577 from voideditor/azure-fix
Fix Azure
2025-05-13 21:06:03 -07:00
Andrew Pareles 4b9e2c2319 azure link 2025-05-13 21:02:44 -07:00
Andrew Pareles 19bd649451 Merge pull request #579 from voideditor/model-selection
Fix Grok 3
2025-05-13 21:01:05 -07:00
Andrew Pareles 7fd06b946b fallback 2025-05-13 21:00:47 -07:00
Andrew Pareles 8c760980b6 fix grok 3 2025-05-13 21:00:20 -07:00
Andrew Pareles 96000273cf payload 2025-05-13 20:05:35 -07:00
Andrew Pareles e771edbe2e update azure 2025-05-13 20:04:40 -07:00
Andrew Pareles aafe556333 azure via AzureOpenAI 2025-05-13 20:00:39 -07:00
Andrew Pareles 9502f563f4 Merge pull request #576 from voideditor/model-selection
Refresh blank window
2025-05-13 19:56:11 -07:00
Andrew Pareles 78c6baed82 revert 2025-05-13 19:54:45 -07:00
Andrew Pareles bafbdd9df3 Merge pull request #575 from voideditor/model-selection
fix anthropic empty err message
2025-05-13 19:53:20 -07:00
Joaquin Coromina 4cca689c94 Moved all watcher and file creation logic to initialize and fixed bug with this._configFileExists 2025-05-13 22:24:32 -04:00
Joaquin Coromina 63083cec89 Added MCP section to void settings 2025-05-13 22:23:49 -04:00
Joaquin Coromina 661908237b Added functions to watch, update, and remove watcher for mcpConfigFile 2025-05-13 21:54:42 -04:00
Andrew Pareles b35dfdf475 trigger resize 2025-05-13 18:29:04 -07:00
Joaquin Coromina 2187641cc3 Updated all functions to be private initially 2025-05-13 21:07:06 -04:00
Andrew Pareles 609f4c9cbf UI 2025-05-13 17:27:46 -07:00
Andrew Pareles 6df3b588df fix anthropic empty err message 2025-05-13 17:25:29 -07:00
Joaquin Coromina 01329382b9 Made createMCPConfigFile private 2025-05-13 02:46:48 -04:00
Joaquin Coromina 7af9973a8d Fixed the structure of createMCPConfigFile 2025-05-13 02:37:54 -04:00
Joaquin Coromina 1ed79ef985 Added function to create mcp config file if it doesnt exist 2025-05-13 02:37:06 -04:00
Joaquin Coromina 8a12505ffd Moved mcp.json into a private variable in the class 2025-05-13 02:04:11 -04:00
Joaquin Coromina f82c15401a Updated services.tsx to include IMCPConfigService 2025-05-13 02:01:23 -04:00
Joaquin Coromina b4dff124e0 Updated file with class MCPConfigService and functions to check if mcp.json config file exists or not 2025-05-13 02:00:50 -04:00
Joaquin Coromina 1aeb62d075 Created mcpConfigService file 2025-05-13 01:13:05 -04:00
Andrew Pareles 4b86bfa184 Merge pull request #562 from voideditor/model-selection
bump version
2025-05-12 20:16:26 -07:00
Andrew Pareles f4c1321f25 rm thinking from 2 flash 2025-05-12 20:08:54 -07:00
Andrew Pareles b54dd7fedc 1.3.7 / 0031 2025-05-12 19:53:58 -07:00
Andrew Pareles 7c448f15db Merge pull request #560 from voideditor/model-selection
Azure?
2025-05-12 19:50:57 -07:00
Andrew Pareles 3241178238 Merge remote-tracking branch 'origin/main' into model-selection 2025-05-12 19:50:35 -07:00
Andrew Pareles 68bb3f6962 fix azure? 2025-05-12 19:48:50 -07:00
Andrew Pareles 2e64f5a24b overrides link update 2025-05-12 19:46:18 -07:00
Andrew Pareles 2e5ecb291d comment 2025-05-12 19:44:42 -07:00
Andrew Pareles f1e24c9b4c Merge pull request #545 from HunterEvangelista/510-openai-additional-json
added optional json config for VoidStaticModelInfo
2025-05-12 19:42:22 -07:00
Andrew Pareles f65e4f51a2 misc 2025-05-12 19:40:39 -07:00
Andrew Pareles f1a5d20150 Merge pull request #505 from homanp/feat/apply-to-terminal
feat: add support for applying `shellscript` and `bash` directly to terminal
2025-05-12 19:32:24 -07:00
Andrew Pareles 28dcdff189 isdisabled fix 2025-05-12 19:31:05 -07:00
Andrew Pareles 67ed52415e apply terminal? 2025-05-12 19:29:45 -07:00
Andrew Pareles 3772cf98ea delete 2025-05-12 18:47:40 -07:00
Andrew Pareles 1c6edf7023 generalize 2025-05-12 18:45:33 -07:00
Andrew Pareles 6b6528c3fb Merge pull request #507 from steaks/explorer-view-copy-prompt
Add Void: Copy Prompt as menu item to the ExplorerContext window
2025-05-12 18:30:21 -07:00
Andrew Pareles f98894ad77 format 2025-05-12 18:22:59 -07:00
Andrew Pareles f8745d8b40 Merge remote-tracking branch 'origin/main' into pr/steaks/507 2025-05-12 18:20:55 -07:00
Andrew Pareles 7066b4285b file and folder copy uniform 2025-05-12 18:20:44 -07:00
Andrew Pareles 1eee07e697 Merge pull request #548 from ujjwaljainnn/ujjwaljainnn-add-context-on-add-to-chat
fix: preserve selection when adding to chat via CTRL+L with closed panel
2025-05-12 16:50:04 -07:00
Andrew Pareles 45f7e4b157 minor updates 2025-05-12 16:48:27 -07:00
Andrew Pareles 1619a3c19b Merge pull request #559 from vrtnis/mistral-medium-capabilities
feat(models): update modelCapabilities with mistral-medium entry
2025-05-12 14:37:40 -07:00
vrtnis d668348244 feat(models): update modelCapabilities with mistral-medium entry 2025-05-12 20:58:55 +00:00
Andrew Pareles 3fa680e8ba identify more shells 2025-05-12 12:09:55 -07:00
Andrew Pareles e8b7c9c470 always create terminal when apply 2025-05-12 11:56:08 -07:00
Andrew Pareles a0eb7d6192 Merge remote-tracking branch 'origin/main' into pr/homanp/505 2025-05-12 11:52:22 -07:00
Andrew Pareles 5c4785d691 Merge remote-tracking branch 'origin/model-selection' into pr/homanp/505 2025-05-12 11:50:36 -07:00
Andrew Pareles 1de90cc39d use 1st terminal id 2025-05-12 11:50:25 -07:00
Andrew Pareles 20dba6f04e Merge pull request #556 from voideditor/model-selection
delete
2025-05-12 12:17:21 -06:00
Andrew Pareles 8e0d29ff75 Merge pull request #556 from voideditor/model-selection
delete
2025-05-12 12:14:54 -06:00
Andrew Pareles 117feaa063 Merge pull request #555 from voideditor/model-selection
extensions warning fix by deleting some extensions once
2025-05-12 12:14:54 -06:00
Andrew Pareles d09a41acd3 Merge pull request #556 from voideditor/model-selection
delete
2025-05-12 11:03:17 -07:00
Andrew Pareles 24102fc2b2 log2 2025-05-12 11:00:05 -07:00
Andrew Pareles c2b65eb16f log 2025-05-12 10:59:53 -07:00
Andrew Pareles a4046bc5a5 update so edit extensions.json 2025-05-12 10:58:18 -07:00
Andrew Pareles 6f73804e94 update 2025-05-12 10:50:28 -07:00
Steven Wexler 4300df420c Refine VoidSCM TODO comments 2025-05-12 11:48:47 -06:00
Andrew Pareles 76155beaad Merge pull request #555 from voideditor/model-selection
extensions warning fix by deleting some extensions once
2025-05-12 10:45:08 -07:00
Andrew Pareles 53c68b1267 bump 2025-05-12 10:44:43 -07:00
Andrew Pareles afbc1b40bf extensions warning fix by deleting some extensions once 2025-05-12 10:42:40 -07:00
Steven Wexler 74d5c86593 Add generate commit message button for git 2025-05-12 08:44:05 -06:00
Ismail Pelaseyed 9a905f6cb2 add support for stopping the terminal command 2025-05-12 09:06:22 +02:00
Ujjwal Jain dd42e40a8d fix: preserve selection when adding to chat via CTRL+L with closed panel 2025-05-11 22:01:04 -04:00
Hunter Evangelista 970fd7ecbb moved payload to sdk initialization 2025-05-11 17:10:07 -07:00
Hunter Evangelista 50710143eb Merge branch 'main' into 510-openai-additional-json 2025-05-11 16:06:11 -07:00
Hunter Evangelista d6295e5d65 added optional json config for VoidStaticModelInfo 2025-05-11 16:02:17 -07:00
Andrew Pareles 053a022ef8 Merge pull request #538 from voideditor/model-selection
1.3.5
2025-05-10 22:28:33 -07:00
Andrew Pareles 3b963d8717 1.3.5 2025-05-10 22:28:15 -07:00
Andrew Pareles e8540e4668 Merge pull request #537 from voideditor/model-selection
Cmd L better
2025-05-10 22:27:36 -07:00
Andrew Pareles 39f2f24b42 1.3.4 2025-05-10 22:07:26 -07:00
Andrew Pareles e852874be7 cmd L to add file 2025-05-10 22:05:50 -07:00
Andrew Pareles 6d07f9db14 Reasoning improvements + Gemini (Merge pull request #521 from voideditor/model-selection)
Gemini reasoning improvements + link URI improvements
2025-05-10 18:10:34 -07:00
Andrew Pareles 2934f9a3c5 += -> = 2025-05-10 18:10:00 -07:00
Andrew Pareles 7748dc4f2d misc fixes + (empty message) improvements 2025-05-10 18:06:58 -07:00
Andrew Pareles 8b45aea8a8 fix link + add gemini tool id if empty 2025-05-10 16:28:54 -07:00
Andrew Pareles cab8dd64b6 rm comments 2025-05-10 16:13:36 -07:00
Andrew Pareles 19782c80df voidRelease 2025-05-10 16:09:06 -07:00
Andrew Pareles 926db45f0a 33 2025-05-10 16:03:06 -07:00
Andrew Pareles 4068911d49 27 2025-05-10 16:02:56 -07:00
Andrew Pareles dcee2e2657 add prompt 2025-05-10 16:02:37 -07:00
Andrew Pareles 4c1c0b591a GoogleGenerativeAI (deprecated) -> genai, add gemini reasoning, improve reasoning logic 2025-05-10 15:59:26 -07:00
Andrew Pareles 61fc593903 shorten non URL links too 2025-05-10 15:53:05 -07:00
Andrew Pareles ecff59eed7 shorten long URI links 2025-05-10 15:29:55 -07:00
Ismail Pelaseyed e2a7b0e1a0 fix merge conflicts 2025-05-10 13:41:43 +02:00
Andrew Pareles 07c3f33f1e void increment 2025-05-10 03:36:54 -07:00
Andrew Pareles 0f93e1b055 Merge pull request #518 from voideditor/model-selection
Model selection
2025-05-10 03:33:45 -07:00
Andrew Pareles 2106573922 misc 2025-05-10 03:32:57 -07:00
Andrew Pareles 93a13a76af Merge remote-tracking branch 'origin/main' into model-selection 2025-05-10 03:31:45 -07:00
Andrew Pareles 7b18e25288 period 2025-05-10 03:29:33 -07:00
Andrew Pareles 57b0505a7e Merge pull request #517 from voideditor/revert-516-model-selection
apply UX message
2025-05-10 03:28:59 -07:00
Andrew Pareles e7436575a5 Revert "apply button failure better UX" 2025-05-10 03:28:39 -07:00
Andrew Pareles 58d95035ff update message 2025-05-10 03:28:29 -07:00
Andrew Pareles 96436ac461 Merge pull request #516 from voideditor/model-selection
apply button failure better UX
2025-05-10 03:26:48 -07:00
Andrew Pareles 85b1f0ec04 apply button failure better UX 2025-05-10 03:20:03 -07:00
Andrew Pareles 5232b4c30c update prompt to work better in agent 2025-05-10 02:28:40 -07:00
Andrew Pareles 81fd74d0b0 Merge pull request #515 from voideditor/model-selection
fix linux update message
2025-05-10 02:10:03 -07:00
Andrew Pareles 93b727198e fix linux update message 2025-05-10 02:09:49 -07:00
Andrew Pareles f33cf14b07 Update README.md 2025-05-09 17:13:35 -07:00
Andrew Pareles 15cf383524 Update README.md 2025-05-09 17:13:15 -07:00
Andrew Pareles f7489375b0 Update issue_template.md 2025-05-09 17:03:16 -07:00
Steven Wexler 08274637e1 Small touchups 2025-05-09 15:40:34 -06:00
Steven Wexler 168b92f6c4 Add Void: Copy Prompt as menu item to the ExplorerContext window 2025-05-09 15:38:32 -06:00
Ismail Pelaseyed a9aa9a26ea feat: add support for applying shellscripts and bash directly to terminal 2025-05-09 14:59:27 +02:00
Andrew Pareles eeb6167c12 Merge pull request #504 from voideditor/model-selection
do not show X / Check while streaming
2025-05-09 05:03:19 -07:00
Andrew Pareles c2092779e7 UI around tools 2025-05-09 05:02:08 -07:00
Andrew Pareles 19dc26d35b Merge pull request #502 from voideditor/model-selection
terminal 10s
2025-05-09 04:42:30 -07:00
Andrew Pareles b1c17256c2 terminal 10s 2025-05-09 04:41:52 -07:00
Andrew Pareles 1992a86211 Merge pull request #501 from voideditor/model-selection
Terminal improvements
2025-05-09 03:43:03 -07:00
Andrew Pareles 70fe79c4fe add terminal 2025-05-09 03:41:46 -07:00
Andrew Pareles a1bf83b5a6 faster 2025-05-09 03:35:18 -07:00
Andrew Pareles 215efed10c disposables 2025-05-09 02:03:23 -07:00
Andrew Pareles 6ada92e6f5 terminal tool improvements 2025-05-09 01:39:15 -07:00
Andrew Pareles ae55e97c18 add background terminal (not jittery) 2025-05-09 01:06:27 -07:00
Andrew Pareles 07c9a72c83 openrouter gemini fix 2025-05-08 23:53:19 -07:00
Andrew Pareles e577d4a01b Update README.md 2025-05-08 17:11:34 -07:00
Andrew Pareles 56c2f7a900 Merge pull request #491 from voideditor/model-selection
ui
2025-05-08 13:48:59 -07:00
Andrew Pareles 558bec1f61 not blue 2025-05-08 13:48:21 -07:00
Andrew Pareles 12e97f5689 ui 2025-05-08 13:34:23 -07:00
Andrew Pareles 7220323098 fix 2025-05-08 13:32:01 -07:00
Andrew Pareles 94229ff1d8 Merge pull request #490 from voideditor/model-selection
Onboarding UI
2025-05-08 13:22:21 -07:00
Andrew Pareles 3b778a3c36 spacing 2025-05-08 13:22:04 -07:00
Andrew Pareles f6529f2d76 better onboarding 2025-05-08 13:20:35 -07:00
Mathew Pareles 1b222e8335 Update README.md 2025-05-08 12:53:32 -07:00
Andrew Pareles 3b518e6f96 Merge pull request #486 from voideditor/model-selection
remove reapply
2025-05-08 12:12:49 -07:00
Andrew Pareles ac23cbb523 remove reapply 2025-05-08 12:11:54 -07:00
Andrew Pareles 301418bd12 Merge pull request #474 from voideditor/model-selection
Accept|Reject on Edit tool fix
2025-05-08 12:07:01 -07:00
Mathew Pareles d192bdcdc0 selecitons work with link 2025-05-08 04:22:50 -07:00
Mathew Pareles 42f2958959 desc1 link 2025-05-08 03:53:18 -07:00
Andrew Pareles 7a3cf4953d misc 2025-05-08 03:41:02 -07:00
Andrew Pareles 84ddf2b88c prompt 2025-05-08 02:40:13 -07:00
Andrew Pareles d00c478c5b add accept|reject on edit tool! 2025-05-08 02:21:57 -07:00
Mathew Pareles 42391a33c8 tooltip 2025-05-08 01:48:41 -07:00
Mathew Pareles 4618442c11 num messages better 2025-05-07 20:12:51 -07:00
Andrew Pareles 77d174edcf Merge pull request #471 from voideditor/model-selection
Misc fixes
2025-05-07 19:56:13 -07:00
Andrew Pareles 79d549b45b release fix 2025-05-07 19:52:54 -07:00
Mathew Pareles 8ba5ee5ea3 threads 2025-05-07 19:51:11 -07:00
Mathew Pareles 1d40c14d50 remove logging 2025-05-07 19:42:32 -07:00
Mathew Pareles cc51d2852e improve @ to mention 2025-05-07 19:41:41 -07:00
Andrew Pareles 8855cdeb04 Merge pull request #469 from GhostDog98/patch-3
Update HOW_TO_CONTRIBUTE.md with tip for nvm
2025-05-07 18:00:34 -07:00
ghostoverflow256 559994f1ee Update HOW_TO_CONTRIBUTE.md
Added a tip for those who may be unfamiliar with the node ecosystem, that you can use nvm to do that step (and probably should).
2025-05-08 10:46:19 +10:00
Andrew Pareles 05e3625014 Merge pull request #468 from voideditor/model-selection
Extensions
2025-05-07 14:37:15 -07:00
Andrew Pareles d9951e4108 dont delete, just transfer extensions 2025-05-07 14:35:26 -07:00
Andrew Pareles 620b5b7341 permalink 2025-05-06 14:13:33 -07:00
Andrew Pareles d33b5ff9a3 Merge pull request #465 from voideditor/model-selection
{} for openaicompat
2025-05-06 13:46:30 -07:00
Andrew Pareles 2208bce55f {} for openaicompat 2025-05-06 13:46:08 -07:00
Andrew Pareles bd62b68dfb Merge pull request #464 from voideditor/model-selection
Gemini Experimental
2025-05-06 13:35:04 -07:00
Andrew Pareles af3e0e863b exp free 2025-05-06 13:34:31 -07:00
Andrew Pareles 57f4d8cd38 Merge pull request #463 from voideditor/model-selection
Laggy Extensions Fix
2025-05-06 13:27:16 -07:00
Andrew Pareles 262f3aee12 update gemini models + fix del extension 2025-05-06 13:26:36 -07:00
Andrew Pareles b03cbadb9b auto delete laggy extensions 2025-05-06 13:16:57 -07:00
Andrew Pareles 390afaf227 ignore extensions actually works 2025-05-06 12:47:18 -07:00
Andrew Pareles 41f6ce8f9b Merge pull request #461 from voideditor/model-selection
update copy
2025-05-06 07:48:00 -07:00
Andrew Pareles cbc3d4b52f update copy 2025-05-06 07:41:58 -07:00
Andrew Pareles 38eeee8e30 Merge pull request #459 from voideditor/model-selection
Dump full files into context; better onboarding; better nav
2025-05-06 06:03:15 -07:00
Andrew Pareles 84f95170cb finish 2025-05-06 05:58:30 -07:00
Andrew Pareles 2de4cacf0f 1.3.2 2025-05-06 05:39:26 -07:00
Andrew Pareles 097d89366c file and folder dump! 2025-05-06 05:39:00 -07:00
Andrew Pareles fa83dfc529 onboarding improvements 2025-05-06 05:39:00 -07:00
Mathew Pareles c5862188cb goto next diff after accept/reject 2025-05-06 05:19:14 -07:00
Mathew Pareles 8816c7194d better tooltips 2025-05-06 05:09:44 -07:00
Mathew Pareles 4b40ac1b47 commandbar decent draft 2025-05-06 04:33:48 -07:00
Mathew Pareles 02115f5b6c bug in names 2025-05-06 03:52:28 -07:00
Mathew Pareles 786f5707e3 add keybinds 2025-05-06 03:47:21 -07:00
Andrew Pareles 71d703852b improve onboarding 2025-05-06 02:08:38 -07:00
Andrew Pareles ae179b8111 fix gemini OR 2025-05-05 22:05:01 -07:00
Andrew Pareles 170ccb9298 ui 2025-05-05 21:49:04 -07:00
Andrew Pareles a5b43ce146 Cmd+Shift+L transfers old text+selections, delete sidebarState 2025-05-05 21:44:42 -07:00
Mathew Pareles e0156803ff better commandbar ui 2025-05-05 21:37:41 -07:00
Andrew Pareles fcd445d875 Merge pull request #453 from voideditor/model-selection
Fix gemini OpenRouter
2025-05-05 17:17:57 -07:00
Andrew Pareles decd0c9bc4 openrouter gemini-style -> openai-style 2025-05-05 17:15:37 -07:00
Andrew Pareles e1da2a5231 update 2025-05-05 17:06:25 -07:00
Andrew Pareles 4570068fa9 update commnet 2025-05-05 17:06:06 -07:00
Andrew Pareles afacd32407 sourcecode overrides link 2025-05-05 17:02:35 -07:00
Andrew Pareles fa782243c9 comment 2025-05-05 17:01:06 -07:00
Andrew Pareles 19390808bc update 2025-05-05 17:00:29 -07:00
Andrew Pareles f77258799f update comment for VoidStaticModelInfo 2025-05-05 17:00:07 -07:00
Andrew Pareles f97cdd82d8 Merge pull request #451 from voideditor/model-selection
Azure, Reasoning, Model overrides, Fix Search/Replace without whitespace, File dump, New models
2025-05-05 16:42:05 -07:00
Andrew Pareles 9216a0e91f update voidVersion 2025-05-05 16:40:32 -07:00
Andrew Pareles aa03c34db6 update link 2025-05-05 16:39:52 -07:00
Andrew Pareles d125d8698b comment 2025-05-05 16:38:51 -07:00
Mathew Pareles f56ccad713 revert draft for today's push 2025-05-05 03:25:32 -07:00
Mathew Pareles 4c511f0c32 draft 2025-05-05 03:24:51 -07:00
Mathew Pareles ca3acb0d30 commandbar draft 2025-05-05 03:22:57 -07:00
Andrew Pareles 38e6ee94d8 phi 4 2025-05-05 03:09:38 -07:00
Andrew Pareles 1c9e684350 misc improvements 2025-05-05 02:52:21 -07:00
Mathew Pareles b6e3a57dc6 fix bug with widgetid 2025-05-05 02:30:07 -07:00
Mathew Pareles 7dc86502e3 remove text in @ to mention... 2025-05-05 02:30:07 -07:00
Andrew Pareles e70915afc3 fix model fallbacks - recognizedName 2025-05-05 02:23:27 -07:00
Andrew Pareles fab84d7371 style 2025-05-05 02:03:48 -07:00
Andrew Pareles d38b744dd2 small updates 2025-05-05 01:50:14 -07:00
Andrew Pareles b520523c4c current effort 2025-05-05 01:22:16 -07:00
Andrew Pareles b73c11429c finish adding effort budget 2025-05-05 01:15:53 -07:00
Andrew Pareles 7846b117bd model settings overrides part ii 2025-05-05 01:15:53 -07:00
Andrew Pareles 398a8a1934 misc + model overrides 2025-05-05 01:15:53 -07:00
Andrew Pareles 732269de87 misc 2025-05-05 01:15:52 -07:00
Andrew Pareles 4dae90e047 maxOutputTokens -> reservedOutputTokenSpace 2025-05-05 01:15:52 -07:00
Mathew Pareles 25c12b101f small 2025-05-05 01:10:10 -07:00
Mathew Pareles 09d7e7f3e6 accept/reject better keybind 2025-05-05 00:57:00 -07:00
Mathew Pareles c088ad3c07 keyboard shortcuts for accept/reject 2025-05-05 00:57:00 -07:00
Mathew Pareles 9bebbb5615 draft for accept/reject interactive 2025-05-05 00:57:00 -07:00
Andrew Pareles e726c3c5de add system message trimming because of very small models with 4096 window len 2025-05-04 19:33:29 -07:00
Andrew Pareles d826052444 add model overrides 2025-05-04 19:33:29 -07:00
Mathew Pareles d841468f3b diffidx updates properly 2025-05-03 18:47:01 -07:00
Mathew Pareles 4d04141139 fix backspace 2025-05-03 17:09:43 -07:00
Andrew Pareles 5090e31a89 add model overrides and headers JSON! 2025-05-03 16:41:41 -07:00
Andrew Pareles 4fb0d7d727 providerName 2025-05-02 15:56:36 -07:00
Andrew Pareles 5f12c7a1cf fix submit button click 2025-05-01 22:04:33 -07:00
Andrew Pareles 1af393983b rewrite file tool streaming fix 2025-05-01 21:52:38 -07:00
Andrew Pareles d3d62652db copy 2025-05-01 21:47:54 -07:00
Andrew Pareles 5cd1fa15cf copy 2025-05-01 18:04:35 -07:00
Andrew Pareles 14a276e4a0 flatten settings 2025-05-01 04:04:10 -07:00
Andrew Pareles cf0728f4c6 Merge pull request #443 from voideditor/model-selection
Chat interruption state
2025-05-01 00:45:57 -07:00
Andrew Pareles 6a119e1246 add logs for transfer 2025-05-01 00:45:22 -07:00
Andrew Pareles 8723107f1d voidrules 2025-05-01 00:36:14 -07:00
Andrew Pareles b1ca03611e interruptor and idle chat state fixes 2025-05-01 00:33:14 -07:00
Mathew Pareles 32f41f97d9 bg decent 2025-05-01 00:23:00 -07:00
Mathew Pareles 3580eb01d0 stagingselection bg 2025-04-30 23:05:39 -07:00
Andrew Pareles cb549ddd9a OpenAI-Compatible -> Custom 2025-04-30 22:43:57 -07:00
Andrew Pareles f0f7c57485 voidrules 2025-04-30 22:31:06 -07:00
Andrew Pareles ace3bfdfb6 Merge pull request #441 from voideditor/model-selection
Issue template
2025-04-30 22:28:53 -07:00
Andrew Pareles 90c1beecca do not enable blank issues 2025-04-30 22:28:11 -07:00
Mathew Pareles 4ad286a030 allow user to search from the start of @ file 2025-04-30 22:23:54 -07:00
Andrew Pareles c5bf5e453e Merge pull request #439 from voideditor/model-selection
Edit fixes
2025-04-30 22:14:17 -07:00
Andrew Pareles a89934d7ad gemini 2025-04-30 22:11:58 -07:00
Andrew Pareles e424a2cdbd fix double cancel 2025-04-30 22:00:52 -07:00
Andrew Pareles d9f2fcd700 error UI less overwhelming, gemini separated system message, improve Apply prompt 2025-04-30 21:42:29 -07:00
Mathew Pareles 1ed82ba174 dont show propective selections since we have @ to mention 2025-04-30 21:31:30 -07:00
Mathew Pareles 80a5dc0362 allow backspace stagingselections 2025-04-30 21:28:22 -07:00
Andrew Pareles f166fa3581 native gemini tools, editing UI, tool prompts 2025-04-30 20:27:08 -07:00
Andrew Pareles cec0df989c onerror for edits 2025-04-30 20:27:08 -07:00
Mathew Pareles ceba02ed47 fix jittery @ to mention 2025-04-30 19:26:54 -07:00
Mathew Pareles 1b9fe5b16d bad spacing 2025-04-30 16:47:34 -07:00
Andrew Pareles 8d8fcba833 generating... 2025-04-30 15:14:14 -07:00
Andrew Pareles e02d1e6847 Merge pull request #300 from Brandon-Bai/feature/add-chat-history-removal-button
Add history chat thread remove button in the chat side nav
2025-04-30 14:28:31 -07:00
Andrew Pareles b68f34e644 Merge remote-tracking branch 'origin/main' into pr/Brandon-Bai/300 2025-04-30 13:59:11 -07:00
Andrew Pareles f13279e84a temp 2025-04-30 13:58:51 -07:00
Andrew Pareles 9b861db77f rm debug 2025-04-30 13:49:09 -07:00
Andrew Pareles a8ba44e49e QwQ 2025-04-30 13:48:16 -07:00
Andrew Pareles 1632bb5169 qwen 3 2025-04-30 13:48:07 -07:00
Andrew Pareles 125968feb1 Merge remote-tracking branch 'origin/main' into model-selection 2025-04-30 13:41:59 -07:00
Andrew Pareles d2310973ec qwen 3 2025-04-30 13:39:43 -07:00
Andrew Pareles 722da01c7c Update README.md 2025-04-29 03:55:35 -07:00
Andrew Pareles 244cffe873 Update VOID_CODEBASE_GUIDE.md 2025-04-29 03:53:40 -07:00
Andrew Pareles d5e76813ce Update VOID_CODEBASE_GUIDE.md 2025-04-29 03:52:48 -07:00
Andrew Pareles a663a258f7 Update VOID_CODEBASE_GUIDE.md 2025-04-29 03:52:02 -07:00
Andrew Pareles 51b66338b5 Update README.md 2025-04-29 03:51:19 -07:00
Andrew Pareles b91b2b9276 Update README.md 2025-04-29 03:51:10 -07:00
Andrew Pareles 1081878c31 Update README.md 2025-04-29 03:50:27 -07:00
Andrew Pareles 477dedeb38 Update HOW_TO_CONTRIBUTE.md 2025-04-29 03:49:36 -07:00
Andrew Pareles df721c5370 Merge pull request #414 from Dylan-86/patch-3
Update HOW_TO_CONTRIBUTE.md
2025-04-29 00:59:00 -07:00
Andrew Pareles e493e65cff update 2025-04-29 00:58:22 -07:00
Andrew Pareles c16a45c647 Merge remote-tracking branch 'origin/main' into pr/Dylan-86/414 2025-04-29 00:54:55 -07:00
Andrew Pareles 9871d4f413 update 2025-04-29 00:50:04 -07:00
Andrew Pareles e94c0fb1c0 Merge pull request #436 from voideditor/model-selection
Updates + Rebase
2025-04-29 00:36:35 -07:00
Andrew Pareles 44398eb96b Merge pull request #436 from voideditor/model-selection
Updates + Rebase
2025-04-29 00:36:35 -07:00
Andrew Pareles 413be993d8 interruptions work 2025-04-29 00:25:16 -07:00
Andrew Pareles 106b30dfbd interruptions work 2025-04-29 00:25:16 -07:00
Andrew Pareles 58804620f7 capture toolName 2025-04-29 00:20:13 -07:00
Andrew Pareles 02b03003c0 capture toolName 2025-04-29 00:20:13 -07:00
Andrew Pareles 46d318b0ce voideditor-test -> voideditor 2025-04-29 00:12:46 -07:00
Andrew Pareles f9d8e92621 voideditor-test -> voideditor 2025-04-29 00:12:46 -07:00
Andrew Pareles 806b3b90b5 gitignore 2025-04-29 00:11:57 -07:00
Andrew Pareles 25f1fb4d19 gitignore 2025-04-29 00:11:57 -07:00
Andrew Pareles dfe00e7a18 HOW_TO_CONTRIBUTE 2025-04-29 00:09:40 -07:00
Andrew Pareles 2bd5708915 HOW_TO_CONTRIBUTE 2025-04-29 00:09:40 -07:00
Andrew Pareles c666a5f721 COPY FROM voideditor-test/void (void-test)!!!! 2025-04-29 00:07:20 -07:00
Andrew Pareles d31c0730ed rebase from 17baf84 2025-04-29 00:07:20 -07:00
Dylan-86 a146b8ba0d Update HOW_TO_CONTRIBUTE.md
I added some notes about Ubuntu installing as I ran through a simple npm blocking error when a folder in the Void path contains a space " " like "New Folder". I simply moved to "NewFolder" and the error disappeared.
2025-04-17 12:58:07 +02:00
Shi Bai cbae8d5ec2 Remove one empty line between methods 2025-03-04 21:07:54 -08:00
Andrew Pareles 9f25d652d5 merge 2025-03-01 18:46:10 -08:00
Andrew Pareles be291fd0f9 Merge remote-tracking branch 'origin/main' into pr/Brandon-Bai/300 2025-03-01 18:46:07 -08:00
Andrew Pareles 1ef22641b7 minor changes 2025-03-01 18:19:33 -08:00
Andrew Pareles 334401eb50 Merge remote-tracking branch 'origin/main' into pr/Brandon-Bai/300 2025-03-01 17:52:28 -08:00
Shi Bai f287a5388f Add history chat thread remove button in the chat side nav
- Added a trash icon to the right of the chat thread name to remove a thread history.
- Added 2 icon size constants in SidebarThreadSelector.tsx to avoid the usage of magic numbers.
2025-02-28 16:08:43 -08:00
81 changed files with 8702 additions and 3949 deletions
+1
View File
@@ -0,0 +1 @@
blank_issues_enabled: false
+1 -1
View File
@@ -6,4 +6,4 @@ title: For VSCode-related issues (eg builds), please start the title with `[App]
1. Press `Cmd+Shift+P` in Void, and type `Help: About`. Please paste the information here. Also let us know any other relevant details, like the model and provider you're using if applicable.
2. Describe the issue/feature here.
2. Describe the issue/feature here!
+164
View File
@@ -0,0 +1,164 @@
#!/usr/bin/env python
from __future__ import annotations
import os, sys, json, datetime, pathlib, textwrap, requests
from openai import OpenAI
REPO = "voideditor/void"
CACHE_FILE = pathlib.Path(".github/triage_cache.json")
STAMP_FILE = pathlib.Path(".github/last_triage.txt")
THEMES_MD = textwrap.dedent("""\
1. 🔗 LLM Integration & Provider Support
2. 🖥 App Build & Platform Compatibility
3. 🎯 Prompt, Token, and Cost Management
4. 🧩 Editor UX & Interaction Design
5. 🤖 Agent & Automation Features
6. ⚙️ System Config & Environment Setup
7. 🗃 Meta: Feature Comparison, Structure, and Naming
""").strip()
client = OpenAI(api_key=os.environ["OPENAI_API_KEY"])
headers = {"Authorization": f"Bearer {os.environ['GITHUB_TOKEN']}"}
# ───────── helpers ────────────────────────────────────────────────────────
def utc_iso_now() -> str:
return datetime.datetime.utcnow().replace(microsecond=0, tzinfo=datetime.timezone.utc).isoformat()
def read_stamp() -> str:
return STAMP_FILE.read_text().strip() if STAMP_FILE.exists() else "1970-01-01T00:00:00Z"
def save_stamp():
STAMP_FILE.parent.mkdir(parents=True, exist_ok=True)
STAMP_FILE.write_text(utc_iso_now())
def load_cache() -> dict[int, str]:
return json.loads(CACHE_FILE.read_text()) if CACHE_FILE.exists() else {}
def save_cache(d: dict[int, str]):
CACHE_FILE.parent.mkdir(parents=True, exist_ok=True)
CACHE_FILE.write_text(json.dumps(d, indent=2))
def fetch_open_issues(since_iso: str | None = None) -> list[dict]:
issues, page = [], 1
while True:
url = (
f"https://api.github.com/repos/{REPO}/issues"
f"?state=open&per_page=100&page={page}"
+ (f"&since={since_iso}" if since_iso else "")
)
chunk = requests.get(url, headers=headers).json()
if not chunk or (isinstance(chunk, dict) and chunk.get("message")):
break
issues.extend(i for i in chunk if "pull_request" not in i)
page += 1
return issues
# ───────── main ───────────────────────────────────────────────────────────
last_stamp = read_stamp()
changed = fetch_open_issues(since_iso=last_stamp)
# Fallback if **nothing** changed AND we have *no* existing output
if not changed:
cache_exists = CACHE_FILE.exists()
wiki_exists = pathlib.Path("wiki/Issue-Categories.md").exists()
if not cache_exists or not wiki_exists:
# first run or someone wiped the wiki → build from scratch
print("⏩ First run or empty wiki — fetching ALL open issues.", file=sys.stderr)
changed = fetch_open_issues() # full list
else:
print(f"✅ No issues updated since {last_stamp}. Nothing to classify.", file=sys.stderr)
save_stamp()
sys.exit(0)
# ---------------------------------------------------------------- prompt
issue_lines = "\n".join(f"- {i['title']} ({i['html_url']})" for i in changed)
prompt = textwrap.dedent(f"""\
You are an AI assistant helping triage GitHub issues into exactly 7 predefined themes.
Each issue must go into exactly one of the themes below:
{THEMES_MD}
Format your output in Markdown like:
## 🎯 Prompt, Token, and Cost Management
- [#123](https://github.com/org/repo/issues/123) Title here
Classify these issues:
{issue_lines}
""")
resp = client.chat.completions.create(
model="gpt-4.1",
messages=[{"role": "user", "content": prompt}],
temperature=0.2,
)
md = resp.choices[0].message.content
# ---------------------------------------------------------------- parse GPT
new_map: dict[int, str] = {}
current = None
for ln in md.splitlines():
if ln.startswith("##"):
current = ln.lstrip("# ").strip()
elif ln.lstrip().startswith("- [#"):
try:
num = int(ln.split("[#")[1].split("]")[0])
new_map[num] = current
except Exception:
pass # ignore malformed lines
cache = load_cache()
cache.update(new_map)
save_cache(cache)
save_stamp()
# ---------------------------------------------------------------- rebuild wiki
order = [
"🔗 LLM Integration & Provider Support",
"🖥 App Build & Platform Compatibility",
"🎯 Prompt, Token, and Cost Management",
"🧩 Editor UX & Interaction Design",
"🤖 Agent & Automation Features",
"⚙️ System Config & Environment Setup",
"🗃 Meta: Feature Comparison, Structure, and Naming",
]
sections: dict[str, list[int]] = {t: [] for t in order}
# ── fetch ALL current open issues once (PRs filtered out) ────────────────
title_map: dict[int, tuple[str, str]] = {}
open_now: set[int] = set()
page = 1
while True:
batch = fetch_open_issues(since_iso=None) if page == 1 else []
if not batch:
break
for it in batch:
num = it["number"]
title_map[num] = (it["title"], it["html_url"])
open_now.add(num)
page += 1
# 🧹 drop any cached IDs that are no longer open issues (e.g., became a PR or were closed)
for stale in set(cache) - open_now:
del cache[stale]
save_cache(cache) # persist cleaned cache
# build sections from cleaned cache
for num, theme in cache.items():
if theme in sections: # extra safety
sections[theme].append(num)
# ---------------------------------------------------------------- print roadmap
for theme in order:
issues = sections[theme]
if issues:
print(f"## {theme}")
for n in sorted(issues):
title, url = title_map.get(n, ("(missing)", f"https://github.com/{REPO}/issues/{n}"))
print(f"- [#{n}]({url}) {title}")
print()
+60
View File
@@ -0,0 +1,60 @@
name: Issue Triage to Wiki
on:
workflow_dispatch:
schedule:
- cron: '0 */6 * * *' # every 6 hrs (UTC)
jobs:
roadmap:
runs-on: ubuntu-latest
steps:
# 1️⃣ Check out code (so the script and cache files are present)
- name: Checkout code
uses: actions/checkout@v3
with:
fetch-depth: 1 # shallow clone
# 2️⃣ Set up Python
- name: Set up Python
uses: actions/setup-python@v4
with:
python-version: '3.11'
# 3️⃣ Install dependencies
- name: Install Python dependencies
run: |
pip install openai requests
# 4️⃣ Clone your forks Wiki
- name: Clone your fork's Wiki
run: |
git clone https://x-access-token:${{ secrets.WIKI_TOKEN }}@github.com/${{ github.repository }}.wiki.git wiki
# 5️⃣ (Optional) Show repo tree for debugging
- name: Show repo tree (debug)
run: |
echo "PWD: $(pwd)"
ls -al
ls -al .github/scripts || true
ls -al void/.github/scripts || true
# 6️⃣ Generate roadmap and push only if it changed
- name: Generate roadmap directly into wiki
run: |
python .github/scripts/issue_triage.py > wiki/_new.md
if ! cmp -s wiki/_new.md wiki/Issue-Categories.md ; then
mv wiki/_new.md wiki/Issue-Categories.md
cd wiki
git config user.name "github-actions[bot]"
git config user.email "41898282+github-actions[bot]@users.noreply.github.com"
git add Issue-Categories.md
git commit -m "Auto-update Issue-Categories.md from GPT triage"
git push
else
echo "No content change skipping wiki update"
fi
env:
OPENAI_API_KEY: ${{ secrets.OPENAI_API_KEY }}
GITHUB_TOKEN: ${{ secrets.WIKI_TOKEN }}
+7
View File
@@ -21,3 +21,10 @@ vscode.db
product.overrides.json
*.snap.actual
.vscode-test
# Void added these:
.tmp/
.tmp2/
.tool-versions
src/vs/workbench/contrib/void/browser/react/out/**
src/vs/workbench/contrib/void/browser/react/src2/**
+16
View File
@@ -0,0 +1,16 @@
This is a fork of the VSCode repo called Void.
Most code we care about lives in src/vs/workbench/contrib/void.
You may often need to explore the full repo to find relevant parts of code.
Look for services and built-in functions that you might need to use to solve the problem.
In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any'. Find the correct type to apply and use it.
Do not add or remove semicolons to any of my files. Just go with convention and make the least number of changes.
Never modify files outside src/vs/workbench/contrib/void without consulting with the user first.
All types that map from a value A to B should be called bOfA. For example, if you create a hashmap that goes from toolId to toolName, it should be called toolNameOfToolId, etc.
Do not run anything to validate your changes; tell the user what to do instead.
+57 -67
View File
@@ -12,7 +12,7 @@ There are a few ways to contribute:
### Codebase Guide
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to contribute!
We [highly recommend reading this](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) guide that we put together on Void's sourcecode if you'd like to add new features.
The repo is not as intimidating as it first seems if you read the guide!
@@ -20,14 +20,15 @@ Most of Void's code lives in the folder `src/vs/workbench/contrib/void/`.
## Editing Void's Code
## Building Void
If you're making changes to Void's code as a contributor, you'll want to run a local version of Void to make sure your changes worked. Developer mode lets you do this. Here's how to use it.
### a. Mac - Build Prerequisites
### a. Mac - Prerequisites
If you're using a Mac, you need Python and XCode. You probably have these by default.
### b. Windows - Build Prerequisites
### b. Windows - Prerequisites
If you're using a Windows computer, first get [Visual Studio 2022](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=Community) (recommended) or [VS Build Tools](https://visualstudio.microsoft.com/thank-you-downloading-visual-studio/?sku=BuildTools) (not recommended). If you already have both, you might need to run the next few steps on both of them.
@@ -42,33 +43,52 @@ Go to the "Individual Components" tab and select:
Finally, click Install.
### c. Linux - Build Prerequisites
### c. Linux - Prerequisites
First, run `npm install -g node-gyp`. Then:
- Debian (Ubuntu, etc): `sudo apt-get install build-essential g++ libx11-dev libxkbfile-dev libsecret-1-dev libkrb5-dev python-is-python3`.
- Red Hat (Fedora, etc): `sudo dnf install @development-tools gcc gcc-c++ make libsecret-devel krb5-devel libX11-devel libxkbfile-devel`.
- SUSE (openSUSE, etc): `sudo zypper install patterns-devel-C-C++-devel_C_C++ krb5-devel libsecret-devel libxkbfile-devel libX11-devel`.
- Others: see [How to Contribute](https://github.com/microsoft/vscode/wiki/How-to-Contribute).
### d. Building Void from inside VSCode
### Developer Mode Instructions
To build Void, open `void/` inside VSCode. Then open your terminal and run:
Here's how to start changing Void's code. These steps cover everything from cloning Void, to opening a Developer Mode window where you can play around with your updates.
1. `npm install` to install all dependencies.
2. Build Void.
- Press <kbd>Cmd+Shift+B</kbd> (Mac).
- Press <kbd>Ctrl+Shift+B</kbd> (Windows/Linux).
- This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code).
3. Run Void.
- Run `./scripts/code.sh` (Mac/Linux).
- Run `./scripts/code.bat` (Windows).
4. Nice-to-knows.
- 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).
1. `git clone https://github.com/voideditor/void` to clone the repo.
2. `npm install` to install all dependencies.
3. Open Void or VSCode, and initialize Developer Mode (this can take ~5 min to finish, it's done when 2 of the 3 spinners turn to check marks):
- Windows: Press <kbd>Ctrl+Shift+B</kbd>.
- Mac: Press <kbd>Cmd+Shift+B</kbd>.
- Linux: Press <kbd>Ctrl+Shift+B</kbd>.
4. Open the Void Developer Mode window:
- Windows: `./scripts/code.bat`.
- Mac: `./scripts/code.sh`.
- Linux: `./scripts/code.sh`.
5. You're good to start editing Void's code!
- You won't see your changes unless you press <kbd>Ctrl+R</kbd> (<kbd>Cmd+R</kbd>) inside the new window to reload. Alternatively, press <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 command in step 4, which lets you reset any IDE changes you made by deleting the `.tmp` folder.
- You can kill any of the build scripts by pressing `Ctrl+D` in its terminal. If you press `Ctrl+C` the script will close but will keep running in the background.
If you get any errors, scroll down for common fixes.
#### Common Fixes
- Make sure you followed the prerequisite steps above.
- Make sure you have Node version `20.18.2` (the version in `.nvmrc`).
- You can do this without changing your global Node version using [nvm](https://github.com/nvm-sh/nvm): run `nvm install`, followed by `nvm use` to install the version in `.nvmrc` locally.
- Make sure the path to your Void folder does not have any spaces in it.
- If you get `"TypeError: Failed to fetch dynamically imported module"`, make sure all imports end with `.js`.
- If you get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`.
- If you see missing styles, wait a few seconds and then reload.
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, when running ./scripts/code.sh, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
- If you get errors like `The SUID sandbox helper binary was found, but is not configured correctly` when running ./scripts/code.sh, run
`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` and then run `./scripts/code.sh` again.
- If you have any other 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.
#### Building Void from Terminal
To build Void from the terminal instead of from inside VSCode, follow the steps above, but instead of pressing <kbd>Cmd+Shift+B</kbd>, run `npm run watch`. The build is done when you see something like this:
@@ -81,48 +101,47 @@ To build Void from the terminal instead of from inside VSCode, follow the steps
```
#### Common Fixes
- Make sure you followed the prerequisite steps above.
- 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 get an error with React, try running `NODE_OPTIONS="--max-old-space-size=8192" npm run buildreact`.
- If you see missing styles, wait a few seconds and then reload.
- If you get errors like `npm error libtool: error: unrecognised option: '-static'`, when running ./scripts/code.sh, make sure you have GNU libtool instead of BSD libtool (BSD is the default in macos)
- If you get erorrs like `The SUID sandbox helper binary was found, but is not configured correctly` when running ./scripts/code.sh, run
`sudo chown root:root .build/electron/chrome-sandbox && sudo chmod 4755 .build/electron/chrome-sandbox` and then run `./scripts/code.sh` again.
- If you have any other 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.
### Distributing
Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder).
If you want to completely control Void's build pipeline for your own internal usage, which comes with a lot of time cost (and is typically not recommended), see our [`void-builder`](https://github.com/voideditor/void-builder) repo which builds Void and contains a few important notes about auto-updating and rebasing.
## Packaging
#### Building a Local Executible
We don't usually recommend building a local executible of Void - typically you should follow the steps above to distribute a complete executible with the advantages of VSCodium baked-in, or you should just use Developer Mode to run Void locally which is much faster. If you're certain this is what you want, see details below.
We don't usually recommend packaging. Instead, you should probably just build. If you're sure you want to package Void into an executable app, make sure you've built first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below). Be patient - packaging can take ~25 minutes.
<details>
<summary> Building Locally (not recommended)</summary>
If you're certain you want to build a local executible of Void, follow these steps. It can take ~25 minutes.
Make sure you've already entered Developer Mode with Void first, then run one of the following commands. This will create a folder named `VSCode-darwin-arm64` or similar outside of the void/ repo (see below).
### Mac
##### Mac
- `npm run gulp vscode-darwin-arm64` - most common (Apple Silicon)
- `npm run gulp vscode-darwin-x64` (Intel)
### Windows
##### Windows
- `npm run gulp vscode-win32-x64` - most common
- `npm run gulp vscode-win32-arm64`
### Linux
##### Linux
- `npm run gulp vscode-linux-x64` - most common
- `npm run gulp vscode-linux-arm64`
### Output
##### Local Executible Output
This will generate a folder outside of `void/`:
The local executible will be located in a folder outside of `void/`:
```bash
workspace/
├── void/ # Your Void fork
└── VSCode-darwin-arm64/ # Generated output
```
### Distributing
Void's maintainers distribute Void on our website and in releases. Our build pipeline is a fork of VSCodium, and it works by running GitHub Actions which create the downloadables. The build repo with more instructions lives [here](https://github.com/voideditor/void-builder).
</details>
## Pull Request Guidelines
@@ -134,32 +153,3 @@ Void's maintainers distribute Void on our website and in releases. Our build pip
<!--
# Relevant files
We keep track of all the files we've changed with Void so it's easy to rebase:
Edit: far too many changes to track... this is old
- README.md
- CONTRIBUTING.md
- VOID_USEFUL_LINKS.md
- product.json
- package.json
- src/vs/workbench/api/common/{extHost.api.impl.ts | extHostApiCommands.ts}
- src/vs/workbench/workbench.common.main.ts
- src/vs/workbench/contrib/void/\*
- extensions/void/\*
- .github/\*
- .vscode/settings/\*
- .eslintrc.json
- build/hygiene.js
- build/lib/i18n.resources.json
- build/npm/dirs.js
- vscode.proposed.editorInsets.d.ts - not modified, but code copied
-->
+37 -16
View File
@@ -1,3 +1,30 @@
## Void is now deprecated.
Void is deprecated and no longer accepting contributions.
Thank you to everyone who contributed, both with lines of code and support from the community. Void remains open source and is still one of the best references to use when forking VS Code.
## Download
To view a list of newer Void forks, see [Void Forks](http://github.com/voideditor/void-forks/).
To download an old version of Void, see [Releases](https://github.com/voideditor/void/releases).
## Forking VS Code
If you're forking VS Code, you might still want to reference Void's logic, and see our [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md) and [How to Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
- We mount React + Tailwind. This is not possible in plain VS Code, and required extending the build pipeline to compile React and [scope](https://github.com/andrewpareles/scope-tailwind) Tailwind ourselves.
- You can copy our GitHub Actions to package, sign, and auto-update Void. VS Code's build pipeline is private, so this is normally very hard.
- Our AI provider code is built from scratch, allowing us to support autocomplete (FIM) and other custom responses. We expose grammars for common `<thinking>` tags, tool tags, etc. Feel free to reference our architecture for using IPC and satisfying CSP.
- Use our custom services to edit files. EditCodeService lets you show diffs as code streams in, even token by token. VoidModelService lets you edit files in the background and syncs OS files with your text buffers.
- Everything we've done is 100% open source. See [repos](https://github.com/orgs/voideditor/repositories) for a complete picture of all the repos that make up Void.
# Welcome to Void.
<div align="center">
@@ -9,32 +36,26 @@
/>
</div>
Void is the open-source Cursor alternative.
Use AI agents on your codebase, checkpoint and visualize changes, and bring any model or host locally. Void sends messages directly to providers without retaining your data.
This repo contains the full sourcecode for Void. We are currently in [open beta](https://voideditor.com/email) for Discord members (see the `announcements` channel), with a waitlist for our official release. If you're new, welcome!
This repo contains the full sourcecode for Void's Desktop app. If you're new, welcome!
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
- 🧭 [Website](https://voideditor.com)
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
- 📝 [Changelog](https://voideditor.com/changelog)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
- 🧭 [Codebase Guide](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md)
## Contributing
1. Feel free to attend a weekly meeting in our Discord channel if you'd like to contribute!
2. To get started working on Void, see [`HOW_TO_CONTRIBUTE`](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
3. We're open to collaborations and suggestions of all types - just reach out.
## Reference
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the VSCode/Void codebase, see [`VOID_CODEBASE_GUIDE`](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to our codebase, see [VOID_CODEBASE_GUIDE](https://github.com/voideditor/void/blob/main/VOID_CODEBASE_GUIDE.md).
For a guide on how to develop your own version of Void, see [HOW_TO_CONTRIBUTE](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md) and [void-builder](https://github.com/voideditor/void-builder).
## Support
Feel free to reach out in our Discord or contact us via email: hello@voideditor.com.
You can always reach us in our [Discord server](https://discord.gg/RSNjgaugJs) or contact us via email at hello@voideditor.com.
+8 -9
View File
@@ -52,7 +52,10 @@ Here's some terminology you might want to know about when working inside VSCode:
- You can run actions as a user by pressing Cmd+Shift+P (opens the command pallete), or you can run them internally by using the commandService to call them by ID. We use actions to register keybinding listeners like Cmd+L, Cmd+K, etc. The nice thing about actions is the user can change the keybindings.
### Internal LLM Message Pipeline
### Void's LLM Message Pipeline
Here's a picture of all the dependencies that are relevent between the time you first send a message through Void's sidebar, and the time a request is sent to your provider.
Sending LLM messages from the main process avoids CSP issues with local providers and lets us use node_modules more easily.
@@ -132,25 +135,24 @@ If you want to know how our build pipeline works, see our build repo [here](http
## VSCode Codebase Guide
The Void team put together this list of links to get up and running with VSCode's sourcecode, the foundation of Void. We hope it's helpful!
For additional references, the Void team put together this list of links to get up and running with VSCode.
<details>
#### Links for Beginners
- [VSCode UI guide](https://code.visualstudio.com/docs/getstarted/userinterface) - covers auxbar, panels, etc.
- [UX guide](https://code.visualstudio.com/api/ux-guidelines/overview) - covers Containers, Views, Items, etc.
#### Links for Contributors
- [How VSCode's sourcecode is organized](https://github.com/microsoft/vscode/wiki/Source-Code-Organization) - this explains where the entry point files are, what `browser/` and `common/` mean, etc. This is the most important read on this whole list! We recommend reading the whole thing.
- [Built-in VSCode styles](https://code.visualstudio.com/api/references/theme-color) - CSS variables that are built into VSCode. Use `var(--vscode-{theme but replacing . with -})`. You can also see their [Webview theming guide](https://code.visualstudio.com/api/extension-guides/webview#theming-webview-content).
#### Misc
- [Every command](https://code.visualstudio.com/api/references/commands) built-in to VSCode - not used often, but here for reference.
- Note: VSCode's repo is the source code for the Monaco editor! An "editor" is a Monaco editor, and it shares the code for ITextModel, etc.
@@ -159,13 +161,10 @@ The Void team put together this list of links to get up and running with VSCode'
Void is no longer an extension, so these links are no longer required, but they might be useful if we ever build an extension again.
- [Files you need in an extension](https://code.visualstudio.com/api/get-started/extension-anatomy).
- [An extension's `package.json` schema](https://code.visualstudio.com/api/references/extension-manifest).
- ["Contributes" Guide](https://code.visualstudio.com/api/references/contribution-points) - the `"contributes"` part of `package.json` is how an extension mounts.
- [The Full VSCode Extension API](https://code.visualstudio.com/api/references/vscode-api) - look on the right side for organization. The [bottom](https://code.visualstudio.com/api/references/vscode-api#api-patterns) of the page is easy to miss but is useful - cancellation tokens, events, disposables.
- [Activation events](https://code.visualstudio.com/api/references/activation-events) you can define in `package.json` (not the most useful).
</details>
-1
View File
@@ -82,7 +82,6 @@ function buildWin32Setup(arch, target) {
productJson['target'] = target;
fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t'));
console.log('RawVersion!!!!!!!!!!!!!!', pkg.version.replace(/-\w+$/, '')) // Void
const quality = product.quality || 'dev';
const definitions = {
NameLong: product.nameLong,
+3 -1
View File
@@ -842,7 +842,9 @@ export default tseslint.config(
'@xterm/xterm',
'yauzl',
'yazl',
'zlib'
'zlib',
// Void added this
'@modelcontextprotocol/sdk/**'
]
},
{
+1 -1
View File
@@ -71,7 +71,7 @@
"type": "string",
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
"scope": "application",
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
},
"remote.SSH.remotePlatform": {
"type": "object",
+165 -165
View File
@@ -8,203 +8,203 @@ import { getVSCodeServerConfig } from './serverConfig';
import SSHConnection from './ssh/sshConnection';
export interface ServerInstallOptions {
id: string;
quality: string;
commit: string;
version: string;
release?: string; // void specific
extensionIds: string[];
envVariables: string[];
useSocketPath: boolean;
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate: string;
id: string;
quality: string;
commit: string;
version: string;
release?: string; // void specific
extensionIds: string[];
envVariables: string[];
useSocketPath: boolean;
serverApplicationName: string;
serverDataFolderName: string;
serverDownloadUrlTemplate: string;
}
export interface ServerInstallResult {
exitCode: number;
listeningOn: number | string;
connectionToken: string;
logFile: string;
osReleaseId: string;
arch: string;
platform: string;
tmpDir: string;
[key: string]: any;
exitCode: number;
listeningOn: number | string;
connectionToken: string;
logFile: string;
osReleaseId: string;
arch: string;
platform: string;
tmpDir: string;
[key: string]: any;
}
export class ServerInstallError extends Error {
constructor(message: string) {
super(message);
}
constructor(message: string) {
super(message);
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
export async function installCodeServer(conn: SSHConnection, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], platform: string | undefined, useSocketPath: boolean, logger: Log): Promise<ServerInstallResult> {
let shell = 'powershell';
let shell = 'powershell';
// detect platform and shell for windows
if (!platform || platform === 'windows') {
const result = await conn.exec('uname -s');
// detect platform and shell for windows
if (!platform || platform === 'windows') {
const result = await conn.exec('uname -s');
if (result.stdout) {
if (result.stdout.includes('windows32')) {
platform = 'windows';
} else if (result.stdout.includes('MINGW64')) {
platform = 'windows';
shell = 'bash';
}
} else if (result.stderr) {
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
platform = 'windows';
}
if (result.stdout) {
if (result.stdout.includes('windows32')) {
platform = 'windows';
} else if (result.stdout.includes('MINGW64')) {
platform = 'windows';
shell = 'bash';
}
} else if (result.stderr) {
if (result.stderr.includes('FullyQualifiedErrorId : CommandNotFoundException')) {
platform = 'windows';
}
if (result.stderr.includes('is not recognized as an internal or external command')) {
platform = 'windows';
shell = 'cmd';
}
}
if (result.stderr.includes('is not recognized as an internal or external command')) {
platform = 'windows';
shell = 'cmd';
}
}
if (platform) {
logger.trace(`Detected platform: ${platform}, ${shell}`);
}
}
if (platform) {
logger.trace(`Detected platform: ${platform}, ${shell}`);
}
}
const scriptId = crypto.randomBytes(12).toString('hex');
const scriptId = crypto.randomBytes(12).toString('hex');
const vscodeServerConfig = await getVSCodeServerConfig();
const installOptions: ServerInstallOptions = {
id: scriptId,
version: vscodeServerConfig.version,
commit: vscodeServerConfig.commit,
quality: vscodeServerConfig.quality,
release: vscodeServerConfig.release,
extensionIds,
envVariables,
useSocketPath,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
};
const vscodeServerConfig = await getVSCodeServerConfig();
const installOptions: ServerInstallOptions = {
id: scriptId,
version: vscodeServerConfig.version,
commit: vscodeServerConfig.commit,
quality: vscodeServerConfig.quality,
release: vscodeServerConfig.release,
extensionIds,
envVariables,
useSocketPath,
serverApplicationName: vscodeServerConfig.serverApplicationName,
serverDataFolderName: vscodeServerConfig.serverDataFolderName,
serverDownloadUrlTemplate: serverDownloadUrlTemplate ?? vscodeServerConfig.serverDownloadUrlTemplate ?? DEFAULT_DOWNLOAD_URL_TEMPLATE,
};
let commandOutput: { stdout: string; stderr: string };
if (platform === 'windows') {
const installServerScript = generatePowerShellInstallScript(installOptions);
let commandOutput: { stdout: string; stderr: string };
if (platform === 'windows') {
const installServerScript = generatePowerShellInstallScript(installOptions);
logger.trace('Server install command:', installServerScript);
logger.trace('Server install command:', installServerScript);
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
const endRegex = new RegExp(`${scriptId}: end`);
// investigate if it's possible to use `-EncodedCommand` flag
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
let command = '';
if (shell === 'powershell') {
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'bash') {
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'cmd') {
const script = installServerScript.trim()
// remove comments
.replace(/^#.*$/gm, '')
// remove empty lines
.replace(/\n{2,}/gm, '\n')
// remove leading spaces
.replace(/^\s*/gm, '')
// escape double quotes (from powershell/cmd)
.replace(/"/g, '"""')
// escape single quotes (from cmd)
.replace(/'/g, `''`)
// escape redirect (from cmd)
.replace(/>/g, `^>`)
// escape new lines (from powershell/cmd)
.replace(/\n/g, '\'`n\'');
const installDir = `$HOME\\${vscodeServerConfig.serverDataFolderName}\\install`;
const installScript = `${installDir}\\${vscodeServerConfig.commit}.ps1`;
const endRegex = new RegExp(`${scriptId}: end`);
// investigate if it's possible to use `-EncodedCommand` flag
// https://devblogs.microsoft.com/powershell/invoking-powershell-with-complex-expressions-using-scriptblocks/
let command = '';
if (shell === 'powershell') {
command = `md -Force ${installDir}; echo @'\n${installServerScript}\n'@ | Set-Content ${installScript}; powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'bash') {
command = `mkdir -p ${installDir.replace(/\\/g, '/')} && echo '\n${installServerScript.replace(/'/g, '\'"\'"\'')}\n' > ${installScript.replace(/\\/g, '/')} && powershell -ExecutionPolicy ByPass -File "${installScript}"`;
} else if (shell === 'cmd') {
const script = installServerScript.trim()
// remove comments
.replace(/^#.*$/gm, '')
// remove empty lines
.replace(/\n{2,}/gm, '\n')
// remove leading spaces
.replace(/^\s*/gm, '')
// escape double quotes (from powershell/cmd)
.replace(/"/g, '"""')
// escape single quotes (from cmd)
.replace(/'/g, `''`)
// escape redirect (from cmd)
.replace(/>/g, `^>`)
// escape new lines (from powershell/cmd)
.replace(/\n/g, '\'`n\'');
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
command = `powershell "md -Force ${installDir}" && powershell "echo '${script}'" > ${installScript.replace('$HOME', '%USERPROFILE%')} && powershell -ExecutionPolicy ByPass -File "${installScript.replace('$HOME', '%USERPROFILE%')}"`;
logger.trace('Command length (8191 max):', command.length);
logger.trace('Command length (8191 max):', command.length);
if (command.length > 8191) {
throw new ServerInstallError(`Command line too long`);
}
} else {
throw new ServerInstallError(`Not supported shell: ${shell}`);
}
if (command.length > 8191) {
throw new ServerInstallError(`Command line too long`);
}
} else {
throw new ServerInstallError(`Not supported shell: ${shell}`);
}
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
} else {
const installServerScript = generateBashInstallScript(installOptions);
commandOutput = await conn.execPartial(command, (stdout: string) => endRegex.test(stdout));
} else {
const installServerScript = generateBashInstallScript(installOptions);
logger.trace('Server install command:', installServerScript);
// Fish shell does not support heredoc so let's workaround it using -c option,
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
}
logger.trace('Server install command:', installServerScript);
// Fish shell does not support heredoc so let's workaround it using -c option,
// also replace single quotes (') within the script with ('\'') as there's no quoting within single quotes, see https://unix.stackexchange.com/a/24676
commandOutput = await conn.exec(`bash -c '${installServerScript.replace(/'/g, `'\\''`)}'`);
}
if (commandOutput.stderr) {
logger.trace('Server install command stderr:', commandOutput.stderr);
}
logger.trace('Server install command stdout:', commandOutput.stdout);
if (commandOutput.stderr) {
logger.trace('Server install command stderr:', commandOutput.stderr);
}
logger.trace('Server install command stdout:', commandOutput.stdout);
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
if (!resultMap) {
throw new ServerInstallError(`Failed parsing install script output`);
}
const resultMap = parseServerInstallOutput(commandOutput.stdout, scriptId);
if (!resultMap) {
throw new ServerInstallError(`Failed parsing install script output`);
}
const exitCode = parseInt(resultMap.exitCode, 10);
if (exitCode !== 0) {
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
}
const exitCode = parseInt(resultMap.exitCode, 10);
if (exitCode !== 0) {
throw new ServerInstallError(`Couldn't install vscode server on remote server, install script returned non-zero exit status`);
}
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
? parseInt(resultMap.listeningOn, 10)
: resultMap.listeningOn;
const listeningOn = resultMap.listeningOn.match(/^\d+$/)
? parseInt(resultMap.listeningOn, 10)
: resultMap.listeningOn;
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
const remoteEnvVars = Object.fromEntries(Object.entries(resultMap).filter(([key,]) => envVariables.includes(key)));
return {
exitCode,
listeningOn,
connectionToken: resultMap.connectionToken,
logFile: resultMap.logFile,
osReleaseId: resultMap.osReleaseId,
arch: resultMap.arch,
platform: resultMap.platform,
tmpDir: resultMap.tmpDir,
...remoteEnvVars
};
return {
exitCode,
listeningOn,
connectionToken: resultMap.connectionToken,
logFile: resultMap.logFile,
osReleaseId: resultMap.osReleaseId,
arch: resultMap.arch,
platform: resultMap.platform,
tmpDir: resultMap.tmpDir,
...remoteEnvVars
};
}
function parseServerInstallOutput(str: string, scriptId: string): { [k: string]: string } | undefined {
const startResultStr = `${scriptId}: start`;
const endResultStr = `${scriptId}: end`;
const startResultStr = `${scriptId}: start`;
const endResultStr = `${scriptId}: end`;
const startResultIdx = str.indexOf(startResultStr);
if (startResultIdx < 0) {
return undefined;
}
const startResultIdx = str.indexOf(startResultStr);
if (startResultIdx < 0) {
return undefined;
}
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
if (endResultIdx < 0) {
return undefined;
}
const endResultIdx = str.indexOf(endResultStr, startResultIdx + startResultStr.length);
if (endResultIdx < 0) {
return undefined;
}
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
const installResult = str.substring(startResultIdx + startResultStr.length, endResultIdx);
const resultMap: { [k: string]: string } = {};
const resultArr = installResult.split(/\r?\n/);
for (const line of resultArr) {
const [key, value] = line.split('==');
resultMap[key] = value;
}
const resultMap: { [k: string]: string } = {};
const resultArr = installResult.split(/\r?\n/);
for (const line of resultArr) {
const [key, value] = line.split('==');
resultMap[key] = value;
}
return resultMap;
return resultMap;
}
function generateBashInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
return `
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
return `
# Server installation script
TMP_DIR="\${XDG_RUNTIME_DIR:-"/tmp"}"
@@ -427,16 +427,16 @@ print_install_results_and_exit 0
}
function generatePowerShellInstallScript({ id, quality, version, commit, release, extensionIds, envVariables, useSocketPath, serverApplicationName, serverDataFolderName, serverDownloadUrlTemplate }: ServerInstallOptions) {
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const downloadUrl = serverDownloadUrlTemplate
.replace(/\$\{quality\}/g, quality)
.replace(/\$\{version\}/g, version)
.replace(/\$\{commit\}/g, commit)
.replace(/\$\{os\}/g, 'win32')
.replace(/\$\{arch\}/g, 'x64')
.replace(/\$\{release\}/g, release ?? '');
const extensions = extensionIds.map(id => '--install-extension ' + id).join(' ');
const downloadUrl = serverDownloadUrlTemplate
.replace(/\$\{quality\}/g, quality)
.replace(/\$\{version\}/g, version)
.replace(/\$\{commit\}/g, commit)
.replace(/\$\{os\}/g, 'win32')
.replace(/\$\{arch\}/g, 'x64')
.replace(/\$\{release\}/g, release ?? '');
return `
return `
# Server installation script
$TMP_DIR="$env:TEMP\\$([System.IO.Path]::GetRandomFileName())"
+1 -1
View File
@@ -38,7 +38,7 @@
"type": "string",
"description": "The URL from where the vscode server will be downloaded. You can use the following variables and they will be replaced dynamically:\n- ${quality}: vscode server quality, e.g. stable or insiders\n- ${version}: vscode server version, e.g. 1.69.0\n- ${commit}: vscode server release commit\n- ${arch}: vscode server arch, e.g. x64, armhf, arm64\n- ${release}: release number",
"scope": "application",
"default": "https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz"
"default": "https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz"
}
}
},
@@ -39,7 +39,7 @@ export class ServerInstallError extends Error {
}
}
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}.${release}/void-reh-${os}-${arch}-${version}.${release}.tar.gz';
const DEFAULT_DOWNLOAD_URL_TEMPLATE = 'https://github.com/voideditor/binaries/releases/download/${version}/void-reh-${os}-${arch}-${version}.tar.gz';
export async function installCodeServer(wslManager: WSLManager, distroName: string, serverDownloadUrlTemplate: string | undefined, extensionIds: string[], envVariables: string[], logger: Log): Promise<ServerInstallResult> {
const scriptId = crypto.randomBytes(12).toString('hex');
+225 -153
View File
@@ -13,11 +13,11 @@
"@anthropic-ai/sdk": "^0.40.0",
"@c4312/eventsource-umd": "^3.0.5",
"@floating-ui/react": "^0.27.8",
"@google/generative-ai": "^0.24.0",
"@google/genai": "^0.13.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.6.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@modelcontextprotocol/sdk": "^1.11.2",
"@parcel/watcher": "2.5.1",
"@types/semver": "^7.5.8",
"@vscode/deviceid": "^0.1.1",
@@ -33,7 +33,6 @@
"@vscode/windows-mutex": "^0.5.0",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
<<<<<<< HEAD
"@xterm/addon-clipboard": "^0.2.0-beta.82",
"@xterm/addon-image": "^0.9.0-beta.99",
"@xterm/addon-ligatures": "^0.10.0-beta.99",
@@ -44,22 +43,11 @@
"@xterm/addon-webgl": "^0.19.0-beta.99",
"@xterm/headless": "^5.6.0-beta.99",
"@xterm/xterm": "^5.6.0-beta.99",
=======
"@xterm/addon-clipboard": "^0.2.0-beta.81",
"@xterm/addon-image": "^0.9.0-beta.98",
"@xterm/addon-ligatures": "^0.10.0-beta.98",
"@xterm/addon-progress": "^0.2.0-beta.4",
"@xterm/addon-search": "^0.16.0-beta.98",
"@xterm/addon-serialize": "^0.14.0-beta.98",
"@xterm/addon-unicode11": "^0.9.0-beta.98",
"@xterm/addon-webgl": "^0.19.0-beta.98",
"@xterm/headless": "^5.6.0-beta.98",
"@xterm/xterm": "^5.6.0-beta.98",
>>>>>>> model-selection
"ajv": "^8.17.1",
"cross-spawn": "^7.0.6",
"diff": "^7.0.0",
"eslint-plugin-react": "^7.37.5",
"google-auth-library": "^9.15.1",
"groq-sdk": "^0.20.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
@@ -311,13 +299,14 @@
"dev": true
},
"node_modules/@azure/core-auth": {
"version": "1.7.2",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.7.2.tgz",
"integrity": "sha512-Igm/S3fDYmnMq1uKS38Ae1/m37B3zigdlZw+kocwEhh5GjyKjPrXKO2J6rzpC1wAxrNil/jX9BJRqBshyjnF3g==",
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-auth/-/core-auth-1.9.0.tgz",
"integrity": "sha512-FPwHpZywuyasDSLMqJ6fhbOK3TqUdviZNF8OqRGA4W5Ewib2lEEZ+pBsYcBa88B2NGO/SEnYPGhyBqNlE8ilSw==",
"dev": true,
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-util": "^1.1.0",
"@azure/core-util": "^1.11.0",
"tslib": "^2.6.2"
},
"engines": {
@@ -460,18 +449,18 @@
}
},
"node_modules/@azure/core-rest-pipeline": {
"version": "1.16.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.16.0.tgz",
"integrity": "sha512-CeuTvsXxCUmEuxH5g/aceuSl6w2EugvNHKAtKKVdiX915EjJJxAwfzNNWZreNnbxHZ2fi0zaM6wwS23x2JVqSQ==",
"version": "1.20.0",
"resolved": "https://registry.npmjs.org/@azure/core-rest-pipeline/-/core-rest-pipeline-1.20.0.tgz",
"integrity": "sha512-ASoP8uqZBS3H/8N8at/XwFr6vYrRP3syTK0EUjDXQy0Y1/AUS+QeIRThKmTNJO2RggvBBxaXDPM7YoIwDGeA0g==",
"dev": true,
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@azure/core-auth": "^1.4.0",
"@azure/core-auth": "^1.8.0",
"@azure/core-tracing": "^1.0.1",
"@azure/core-util": "^1.9.0",
"@azure/core-util": "^1.11.0",
"@azure/logger": "^1.0.0",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"@typespec/ts-http-runtime": "^0.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -491,12 +480,14 @@
}
},
"node_modules/@azure/core-util": {
"version": "1.9.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.9.0.tgz",
"integrity": "sha512-AfalUQ1ZppaKuxPPMsFEUdX6GZPB3d9paR9d/TTL7Ow2De8cJaC7ibi7kWVlFAVPCYo31OcnGymc0R89DX8Oaw==",
"version": "1.12.0",
"resolved": "https://registry.npmjs.org/@azure/core-util/-/core-util-1.12.0.tgz",
"integrity": "sha512-13IyjTQgABPARvG90+N2dXpC+hwp466XCdQXPCRlbWHgd3SJd5Q1VvaBGv6k1BIa4MQm6hAF1UBU1m8QUxV8sQ==",
"dev": true,
"license": "MIT",
"dependencies": {
"@azure/abort-controller": "^2.0.0",
"@typespec/ts-http-runtime": "^0.2.2",
"tslib": "^2.6.2"
},
"engines": {
@@ -1726,33 +1717,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
<<<<<<< HEAD
=======
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/@eslint/eslintrc/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/@eslint/js": {
"version": "9.25.0",
"resolved": "https://registry.npmjs.org/@eslint/js/-/js-9.25.0.tgz",
"integrity": "sha512-iWhsUS8Wgxz9AXNfvfOPFSW4VfMXdVhp1hjkZVhXCrpgh/aLcc45rX6MPu+tIVUWDw0HfNwth7O28M1xDxNf9w==",
>>>>>>> model-selection
"dev": true,
"license": "MIT",
"dependencies": {
@@ -1856,11 +1820,17 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@google/generative-ai": {
"version": "0.24.0",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz",
"integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==",
"node_modules/@google/genai": {
"version": "0.13.0",
"resolved": "https://registry.npmjs.org/@google/genai/-/genai-0.13.0.tgz",
"integrity": "sha512-eaEncWt875H7046T04mOpxpHJUM+jLIljEf+5QctRyOeChylE/nhpwm1bZWTRWoOu/t46R9r+PmgsJFhTpE7tQ==",
"license": "Apache-2.0",
"dependencies": {
"google-auth-library": "^9.14.2",
"ws": "^8.18.0",
"zod": "^3.22.4",
"zod-to-json-schema": "^3.22.4"
},
"engines": {
"node": ">=18.0.0"
}
@@ -2646,9 +2616,9 @@
}
},
"node_modules/@modelcontextprotocol/sdk": {
"version": "1.10.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.10.2.tgz",
"integrity": "sha512-rb6AMp2DR4SN+kc6L1ta2NCpApyA9WYNx3CrTSZvGxq9wH71bRur+zRqPfg0vQ9mjywR7qZdX2RGHOPq3ss+tA==",
"version": "1.11.2",
"resolved": "https://registry.npmjs.org/@modelcontextprotocol/sdk/-/sdk-1.11.2.tgz",
"integrity": "sha512-H9vwztj5OAqHg9GockCQC06k1natgcxWQSRpQcPJf6i5+MWBzfKkRtxGbjQf0X2ihii0ffLZCRGbYV2f2bjNCQ==",
"license": "MIT",
"dependencies": {
"content-type": "^1.0.5",
@@ -4290,6 +4260,21 @@
"url": "https://opencollective.com/typescript-eslint"
}
},
"node_modules/@typespec/ts-http-runtime": {
"version": "0.2.2",
"resolved": "https://registry.npmjs.org/@typespec/ts-http-runtime/-/ts-http-runtime-0.2.2.tgz",
"integrity": "sha512-Gz/Sm64+Sq/vklJu1tt9t+4R2lvnud8NbTD/ZfpZtMiUX7YeVpCA8j6NSW8ptwcoLL+NmYANwqP8DV0q/bwl2w==",
"dev": true,
"license": "MIT",
"dependencies": {
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.0",
"tslib": "^2.6.2"
},
"engines": {
"node": ">=18.0.0"
}
},
"node_modules/@vscode/deviceid": {
"version": "0.1.1",
"resolved": "https://registry.npmjs.org/@vscode/deviceid/-/deviceid-0.1.1.tgz",
@@ -5259,28 +5244,6 @@
"resolved": "https://registry.npmjs.org/ajv-formats/-/ajv-formats-2.1.1.tgz",
"integrity": "sha512-Wx0Kx52hxE7C18hkMEggYlEifqWZtYaRgouJor+WMdPnQyEK13vgEWyVNup7SoeeoLMsr4kf5h6dOW11I15MUA==",
"dev": true,
<<<<<<< HEAD
=======
"license": "MIT",
"dependencies": {
"ajv": "^8.0.0"
},
"peerDependencies": {
"ajv": "^8.0.0"
},
"peerDependenciesMeta": {
"ajv": {
"optional": true
}
}
},
"node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
"integrity": "sha512-YCS/JNFAUyr5vAuhk1DWm1CBxRHW9LbJ2ozWeemrIqpbsqKjHVxYPyi5GC0rjZIT5JxJ3virVTS8wk4i/Z+krw==",
"dev": true,
"license": "MIT",
>>>>>>> model-selection
"dependencies": {
"ajv": "^8.0.0"
},
@@ -6085,6 +6048,15 @@
"node": "*"
}
},
"node_modules/bignumber.js": {
"version": "9.3.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.3.0.tgz",
"integrity": "sha512-EM7aMFTXbptt/wZdMlBv2t8IViwQL+h6SLHosp8Yf0dqJMTnY6iL32opnAB6kAdL0SZPuvcAzFr31o0c/R3/RA==",
"license": "MIT",
"engines": {
"node": "*"
}
},
"node_modules/binary-extensions": {
"version": "2.2.0",
"resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.2.0.tgz",
@@ -6315,6 +6287,12 @@
"node": ">=0.4.0"
}
},
"node_modules/buffer-equal-constant-time": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/buffer-equal-constant-time/-/buffer-equal-constant-time-1.0.1.tgz",
"integrity": "sha512-zRpUiDwd/xk6ADqPMATG8vc9VPrkck7T07OIx0gnjmJAnHnTVXNQG3vfvWNuiZIkwu9KrKdA1iJKfsfTVxE6NA==",
"license": "BSD-3-Clause"
},
"node_modules/buffer-from": {
"version": "1.1.2",
"resolved": "https://registry.npmjs.org/buffer-from/-/buffer-from-1.1.2.tgz",
@@ -8162,6 +8140,15 @@
"integrity": "sha512-I88TYZWc9XiYHRQ4/3c5rjjfgkjhLyW2luGIheGERbNQ6OY7yTybanSpDXZa8y7VUP9YmDcYa+eyq4ca7iLqWA==",
"dev": true
},
"node_modules/ecdsa-sig-formatter": {
"version": "1.0.11",
"resolved": "https://registry.npmjs.org/ecdsa-sig-formatter/-/ecdsa-sig-formatter-1.0.11.tgz",
"integrity": "sha512-nagl3RYrbNv6kQkeJIpt6NJZy8twLB/2vtz6yN9Z4vRKHN4/QZJIEbqohALSgwKdnksuY3k5Addp5lg8sVoVcQ==",
"license": "Apache-2.0",
"dependencies": {
"safe-buffer": "^5.0.1"
}
},
"node_modules/editorconfig": {
"version": "0.15.2",
"resolved": "https://registry.npmjs.org/editorconfig/-/editorconfig-0.15.2.tgz",
@@ -8878,23 +8865,6 @@
"url": "https://opencollective.com/eslint"
}
},
"node_modules/eslint/node_modules/ajv": {
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/eslint/node_modules/glob-parent": {
"version": "6.0.2",
"resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-6.0.2.tgz",
@@ -9419,8 +9389,7 @@
"node_modules/extend": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/extend/-/extend-3.0.2.tgz",
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g==",
"dev": true
"integrity": "sha512-fjquC59cD7CyW6urNXK0FBufkZcoiGG80wTuPujX590cB5Ttln20E2UB4S/WARVqhXffZl2LNgS+gQdPIIim/g=="
},
"node_modules/extend-shallow": {
"version": "3.0.2",
@@ -9535,12 +9504,7 @@
"node_modules/fast-deep-equal": {
"version": "3.1.3",
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
<<<<<<< HEAD
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q=="
=======
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
"license": "MIT"
>>>>>>> model-selection
},
"node_modules/fast-fifo": {
"version": "1.3.2",
@@ -9667,43 +9631,6 @@
"version": "6.12.6",
"resolved": "https://registry.npmjs.org/ajv/-/ajv-6.12.6.tgz",
"integrity": "sha512-j3fVLgvTo527anyYyJOGTYJbG+vnnQYvE0m5mmkc1TK+nxAppkCLMIL0aZ4dblVCNoGShhm+kzE4ZUykBoMg4g==",
<<<<<<< HEAD
=======
"dev": true,
"license": "MIT",
"dependencies": {
"fast-deep-equal": "^3.1.1",
"fast-json-stable-stringify": "^2.0.0",
"json-schema-traverse": "^0.4.1",
"uri-js": "^4.2.2"
},
"funding": {
"type": "github",
"url": "https://github.com/sponsors/epoberezkin"
}
},
"node_modules/file-loader/node_modules/ajv-keywords": {
"version": "3.5.2",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-3.5.2.tgz",
"integrity": "sha512-5p6WTN0DdTGVQk6VjcEju19IgaHudalcfabD7yhDGeA6bcQnmL+CpveLJq/3hvfwd1aof6L386Ougkx6RfyMIQ==",
"dev": true,
"license": "MIT",
"peerDependencies": {
"ajv": "^6.9.1"
}
},
"node_modules/file-loader/node_modules/json-schema-traverse": {
"version": "0.4.1",
"resolved": "https://registry.npmjs.org/json-schema-traverse/-/json-schema-traverse-0.4.1.tgz",
"integrity": "sha512-xbbCH5dCYU5T8LcEhhuh7HJ88HXuW3qsI3Y0zOZFKfZEHcpWiHU/Jxzk629Brsab/mMiHQti9wMP+845RPe3Vg==",
"dev": true,
"license": "MIT"
},
"node_modules/file-loader/node_modules/schema-utils": {
"version": "3.3.0",
"resolved": "https://registry.npmjs.org/schema-utils/-/schema-utils-3.3.0.tgz",
"integrity": "sha512-pN/yOAvcC+5rQ5nERGuwrjLlYvLTbCibnZ1I7B1LaiAz9BRBlE9GMgE/eqV30P7aJQUf7Ddimy/RsbYO/GrVGg==",
>>>>>>> model-selection
"dev": true,
"license": "MIT",
"dependencies": {
@@ -10429,6 +10356,68 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/gaxios": {
"version": "6.7.1",
"resolved": "https://registry.npmjs.org/gaxios/-/gaxios-6.7.1.tgz",
"integrity": "sha512-LDODD4TMYx7XXdpwxAVRAIAuB0bzv0s+ywFonY46k126qzQHT9ygyoa9tncmOiQmmDrik65UYsEkv3lbfqQ3yQ==",
"license": "Apache-2.0",
"dependencies": {
"extend": "^3.0.2",
"https-proxy-agent": "^7.0.1",
"is-stream": "^2.0.0",
"node-fetch": "^2.6.9",
"uuid": "^9.0.1"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gaxios/node_modules/is-stream": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/is-stream/-/is-stream-2.0.1.tgz",
"integrity": "sha512-hFoiJiTl63nn+kstHGBtewWSKnQLpyb155KHheA1l39uvtO9nWIop1p3udqPcUd/xbF1VLMO4n7OI6p7RbngDg==",
"license": "MIT",
"engines": {
"node": ">=8"
},
"funding": {
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/gaxios/node_modules/node-fetch": {
"version": "2.7.0",
"resolved": "https://registry.npmjs.org/node-fetch/-/node-fetch-2.7.0.tgz",
"integrity": "sha512-c4FRfUm/dbcWZ7U+1Wq0AwCyFL+3nt2bEw05wfxSz+DWpWsitgmSgYmy2dQdWyKC1694ELPqMs/YzUSNozLt8A==",
"license": "MIT",
"dependencies": {
"whatwg-url": "^5.0.0"
},
"engines": {
"node": "4.x || >=6.0.0"
},
"peerDependencies": {
"encoding": "^0.1.0"
},
"peerDependenciesMeta": {
"encoding": {
"optional": true
}
}
},
"node_modules/gcp-metadata": {
"version": "6.1.1",
"resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz",
"integrity": "sha512-a4tiq7E0/5fTjxPAaH4jpjkSv/uCaU2p5KC6HVGrvl0cDjA8iBZv4vv1gyzlmK0ZUKqwpOyQMKzZQe3lTit77A==",
"license": "Apache-2.0",
"dependencies": {
"gaxios": "^6.1.1",
"google-logging-utils": "^0.0.2",
"json-bigint": "^1.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/gensync": {
"version": "1.0.0-beta.2",
"resolved": "https://registry.npmjs.org/gensync/-/gensync-1.0.0-beta.2.tgz",
@@ -11065,6 +11054,32 @@
"node": ">= 0.10"
}
},
"node_modules/google-auth-library": {
"version": "9.15.1",
"resolved": "https://registry.npmjs.org/google-auth-library/-/google-auth-library-9.15.1.tgz",
"integrity": "sha512-Jb6Z0+nvECVz+2lzSMt9u98UsoakXxA2HGHMCxh+so3n90XgYWkq5dur19JAJV7ONiJY22yBTyJB1TSkvPq9Ng==",
"license": "Apache-2.0",
"dependencies": {
"base64-js": "^1.3.0",
"ecdsa-sig-formatter": "^1.0.11",
"gaxios": "^6.1.1",
"gcp-metadata": "^6.1.0",
"gtoken": "^7.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14"
}
},
"node_modules/google-logging-utils": {
"version": "0.0.2",
"resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz",
"integrity": "sha512-NEgUnEcBiP5HrPzufUkBzJOD/Sxsco3rLNo1F1TNf7ieU8ryUzBhqba8r756CjLX7rn3fHl6iLEwPYuqpoKgQQ==",
"license": "Apache-2.0",
"engines": {
"node": ">=14"
}
},
"node_modules/gopd": {
"version": "1.2.0",
"resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz",
@@ -11137,6 +11152,19 @@
"undici-types": "~5.26.4"
}
},
"node_modules/gtoken": {
"version": "7.1.0",
"resolved": "https://registry.npmjs.org/gtoken/-/gtoken-7.1.0.tgz",
"integrity": "sha512-pCcEwRi+TKpMlxAQObHDQ56KawURgyAf6jtIY046fJ5tIv3zDe/LEIubckAO8fj6JnAxLdmWkUfNyulQ2iKdEw==",
"license": "MIT",
"dependencies": {
"gaxios": "^6.0.0",
"jws": "^4.0.0"
},
"engines": {
"node": ">=14.0.0"
}
},
"node_modules/gulp": {
"version": "4.0.2",
"resolved": "https://registry.npmjs.org/gulp/-/gulp-4.0.2.tgz",
@@ -14096,6 +14124,15 @@
"node": ">=6"
}
},
"node_modules/json-bigint": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/json-bigint/-/json-bigint-1.0.0.tgz",
"integrity": "sha512-SiPv/8VpZuWbvLSMtTDU8hEfrZWg/mH/nV/b4o0CYbSxu1UIQPLdwKOCIyLQX+VIPO5vrLX3i8qtqFyhdPSUSQ==",
"license": "MIT",
"dependencies": {
"bignumber.js": "^9.0.0"
}
},
"node_modules/json-buffer": {
"version": "3.0.1",
"resolved": "https://registry.npmjs.org/json-buffer/-/json-buffer-3.0.1.tgz",
@@ -14216,6 +14253,27 @@
"integrity": "sha512-g3UB796vUFIY90VIv/WX3L2c8CS2MdWUww3CNrYmqza1Fg0DURc2K/O4YrnklBdQarSJ/y8JnJYDGc+1iumQjg==",
"dev": true
},
"node_modules/jwa": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/jwa/-/jwa-2.0.0.tgz",
"integrity": "sha512-jrZ2Qx916EA+fq9cEAeCROWPTfCwi1IVHqT2tapuqLEVVDKFDENFw1oL+MwrTvH6msKxsd1YTDVw6uKEcsrLEA==",
"license": "MIT",
"dependencies": {
"buffer-equal-constant-time": "1.0.1",
"ecdsa-sig-formatter": "1.0.11",
"safe-buffer": "^5.0.1"
}
},
"node_modules/jws": {
"version": "4.0.0",
"resolved": "https://registry.npmjs.org/jws/-/jws-4.0.0.tgz",
"integrity": "sha512-KDncfTmOZoOMTFG4mBlG0qUIOlc03fmzH+ru6RgYVZhPkyiy/92Owlt/8UEN+a4TXR1FQetfIpJE8ApdvdVxTg==",
"license": "MIT",
"dependencies": {
"jwa": "^2.0.0",
"safe-buffer": "^5.0.1"
}
},
"node_modules/kerberos": {
"version": "2.1.1",
"resolved": "https://registry.npmjs.org/kerberos/-/kerberos-2.1.1.tgz",
@@ -19076,10 +19134,6 @@
"version": "2.0.2",
"resolved": "https://registry.npmjs.org/require-from-string/-/require-from-string-2.0.2.tgz",
"integrity": "sha512-Xf0nWe6RseziFMu+Ap9biiUbmplq6S9/p+7w7YXP/JBHhrUDDUhwa+vANyubuqfZWTveU//DYVGsDG7RKL/vEw==",
<<<<<<< HEAD
=======
"license": "MIT",
>>>>>>> model-selection
"engines": {
"node": ">=0.10.0"
}
@@ -19554,7 +19608,6 @@
"url": "https://opencollective.com/webpack"
}
},
<<<<<<< HEAD
"node_modules/schema-utils/node_modules/ajv-keywords": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/ajv-keywords/-/ajv-keywords-5.1.0.tgz",
@@ -19567,8 +19620,6 @@
"ajv": "^8.8.2"
}
},
=======
>>>>>>> model-selection
"node_modules/scope-tailwind": {
"version": "1.0.9",
"resolved": "https://registry.npmjs.org/scope-tailwind/-/scope-tailwind-1.0.9.tgz",
@@ -23740,6 +23791,27 @@
"resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz",
"integrity": "sha1-tSQ9jz7BqjXxNkYFvA0QNuMKtp8= sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="
},
"node_modules/ws": {
"version": "8.18.2",
"resolved": "https://registry.npmjs.org/ws/-/ws-8.18.2.tgz",
"integrity": "sha512-DMricUmwGZUVr++AEAe2uiVM7UoO9MAVZMDu05UQOaUII0lp+zOzLLU4Xqh/JvTqklB1T4uELaaPBKyjE1r4fQ==",
"license": "MIT",
"engines": {
"node": ">=10.0.0"
},
"peerDependencies": {
"bufferutil": "^4.0.1",
"utf-8-validate": ">=5.0.2"
},
"peerDependenciesMeta": {
"bufferutil": {
"optional": true
},
"utf-8-validate": {
"optional": true
}
}
},
"node_modules/xml": {
"version": "1.0.1",
"resolved": "https://registry.npmjs.org/xml/-/xml-1.0.1.tgz",
+13 -12
View File
@@ -75,11 +75,11 @@
"@anthropic-ai/sdk": "^0.40.0",
"@c4312/eventsource-umd": "^3.0.5",
"@floating-ui/react": "^0.27.8",
"@google/generative-ai": "^0.24.0",
"@google/genai": "^0.13.0",
"@microsoft/1ds-core-js": "^3.2.13",
"@microsoft/1ds-post-js": "^3.2.13",
"@mistralai/mistralai": "^1.6.0",
"@modelcontextprotocol/sdk": "^1.10.2",
"@modelcontextprotocol/sdk": "^1.11.2",
"@parcel/watcher": "2.5.1",
"@types/semver": "^7.5.8",
"@vscode/deviceid": "^0.1.1",
@@ -95,20 +95,21 @@
"@vscode/windows-mutex": "^0.5.0",
"@vscode/windows-process-tree": "^0.6.0",
"@vscode/windows-registry": "^1.1.0",
"@xterm/addon-clipboard": "^0.2.0-beta.81",
"@xterm/addon-image": "^0.9.0-beta.98",
"@xterm/addon-ligatures": "^0.10.0-beta.98",
"@xterm/addon-progress": "^0.2.0-beta.4",
"@xterm/addon-search": "^0.16.0-beta.98",
"@xterm/addon-serialize": "^0.14.0-beta.98",
"@xterm/addon-unicode11": "^0.9.0-beta.98",
"@xterm/addon-webgl": "^0.19.0-beta.98",
"@xterm/headless": "^5.6.0-beta.98",
"@xterm/xterm": "^5.6.0-beta.98",
"@xterm/addon-clipboard": "^0.2.0-beta.82",
"@xterm/addon-image": "^0.9.0-beta.99",
"@xterm/addon-ligatures": "^0.10.0-beta.99",
"@xterm/addon-progress": "^0.2.0-beta.5",
"@xterm/addon-search": "^0.16.0-beta.99",
"@xterm/addon-serialize": "^0.14.0-beta.99",
"@xterm/addon-unicode11": "^0.9.0-beta.99",
"@xterm/addon-webgl": "^0.19.0-beta.99",
"@xterm/headless": "^5.6.0-beta.99",
"@xterm/xterm": "^5.6.0-beta.99",
"ajv": "^8.17.1",
"cross-spawn": "^7.0.6",
"diff": "^7.0.0",
"eslint-plugin-react": "^7.37.5",
"google-auth-library": "^9.15.1",
"groq-sdk": "^0.20.1",
"http-proxy-agent": "^7.0.0",
"https-proxy-agent": "^7.0.2",
+5 -2
View File
@@ -1,7 +1,8 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.2.8",
"voidVersion": "1.4.9",
"voidRelease": "0044",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
@@ -38,6 +39,8 @@
"builtInExtensions": [],
"linkProtectionTrustedDomains": [
"https://voideditor.com",
"https://voideditor.dev"
"https://voideditor.dev",
"https://github.com/voideditor/void",
"https://ollama.com"
]
}
+12 -1
View File
@@ -130,7 +130,9 @@ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpda
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/sendLLMMessageChannel.js';
import { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMainService.js';
import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.js';
import { MCPChannel } from '../../workbench/contrib/void/electron-main/mcpChannel.js';
/**
* The main VS Code application. There will only ever be one instance,
* even if the user starts many instances (e.g. from the command line).
@@ -1102,6 +1104,7 @@ export class CodeApplication extends Disposable {
// Void main process services (required for services with a channel for comm between browser and electron-main (node))
services.set(IMetricsService, new SyncDescriptor(MetricsMainService, undefined, false));
services.set(IVoidUpdateService, new SyncDescriptor(VoidMainUpdateService, undefined, false));
services.set(IVoidSCMService, new SyncDescriptor(VoidSCMService, undefined, false));
// Default Extensions Profile Init
services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true));
@@ -1243,6 +1246,14 @@ export class CodeApplication extends Disposable {
const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService));
mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel);
// Void added this
const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCMService), disposables);
mainProcessElectronServer.registerChannel('void-channel-scm', voidSCMChannel);
// Void added this
const mcpChannel = new MCPChannel();
mainProcessElectronServer.registerChannel('void-channel-mcp', mcpChannel);
// Extension Host Debug Broadcasting
const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService));
mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel);
@@ -163,38 +163,35 @@ export class TelemetryService implements ITelemetryService {
}
function getTelemetryLevelSettingDescription(): string {
const telemetryText = localize('telemetry.telemetryLevelMd', "Controls {0} telemetry, first-party extension telemetry, and participating third-party extension telemetry. Some third party extensions might not respect this setting. Consult the specific extension's documentation to be sure. Telemetry helps us better understand how {0} is performing, where improvements need to be made, and how features are being used.", product.nameLong);
const externalLinksStatement = !product.privacyStatementUrl ?
localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') :
localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl);
const restartString = !isWeb ? localize('telemetry.restart', 'A full restart of the application is necessary for crash reporting changes to take effect.') : '';
const telemetryText = localize('telemetry.telemetryLevelMd', "The default telemetry setting for VS Code (Microsoft). {0} recommends keeping this off.", product.nameLong);
// const externalLinksStatement = !product.privacyStatementUrl ?
// localize("telemetry.docsStatement", "Read more about the [data we collect]({0}).", 'https://aka.ms/vscode-telemetry') :
// localize("telemetry.docsAndPrivacyStatement", "Read more about the [data we collect]({0}) and our [privacy statement]({1}).", 'https://aka.ms/vscode-telemetry', product.privacyStatementUrl);
const restartString = !isWeb ? localize('telemetry.restart', 'Microsoft says \"Some third party extensions might not respect this setting. Consult the specific extension\'s documentation to be sure. A full restart of the application is necessary for crash reporting changes to take effect.\"') : '';
const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports");
const errorsHeader = localize('telemetry.errors', "Error Telemetry");
const usageHeader = localize('telemetry.usage', "Usage Data");
const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:");
const telemetryTable = `
| | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} |
|:------|:-------------:|:---------------:|:----------:|
| all | ✓ | ✓ | ✓ |
| error | ✓ | ✓ | - |
| crash | ✓ | - | - |
| off | - | - | - |
`;
// Void removed these
// const crashReportsHeader = localize('telemetry.crashReports', "Crash Reports");
// const errorsHeader = localize('telemetry.errors', "Error Telemetry");
// const usageHeader = localize('telemetry.usage', "Usage Data");
const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*");
// const telemetryTableDescription = localize('telemetry.telemetryLevel.tableDescription', "The following table outlines the data sent with each setting:");
// const telemetryTable = `
// | | ${crashReportsHeader} | ${errorsHeader} | ${usageHeader} |
// |:------|:-------------:|:---------------:|:----------:|
// | all | ✓ | ✓ | ✓ |
// | error | ✓ | ✓ | - |
// | crash | ✓ | - | - |
// | off | - | - | - |
// `;
// const deprecatedSettingNote = localize('telemetry.telemetryLevel.deprecated', "****Note:*** If this setting is 'off', no telemetry will be sent regardless of other telemetry settings. If this setting is set to anything except 'off' and telemetry is disabled with deprecated settings, no telemetry will be sent.*");
const telemetryDescription = `
${telemetryText} ${externalLinksStatement} ${restartString}
${telemetryText}
&nbsp;
${restartString}
${telemetryTableDescription}
${telemetryTable}
&nbsp;
${deprecatedSettingNote}
Void separately records basic usage like the number of messages people are sending. If you'd like to disable Void metrics, you may do so in Void's Settings.
`;
return telemetryDescription;
@@ -26,7 +26,6 @@ import { IViewsService } from '../../../services/views/common/viewsService.js';
/* eslint-disable */ // Void
import { VOID_CTRL_K_ACTION_ID, VOID_CTRL_L_ACTION_ID } from '../../../contrib/void/browser/actionIDs.js';
import { VOID_OPEN_SETTINGS_ACTION_ID } from '../../../contrib/void/browser/voidSettingsPane.js';
import { VIEWLET_ID as REMOTE_EXPLORER_VIEWLET_ID } from '../../../contrib/remote/browser/remoteExplorer.js';
/* eslint-enable */
@@ -306,21 +305,21 @@ export class EditorGroupWatermark extends Disposable {
label2.set(keys2);
this.currentDisposables.add(label2);
const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
const button3 = append(recentsBox, $('button'));
button3.textContent = `Void Settings`
button3.style.display = 'block'
button3.style.marginLeft = 'auto'
button3.style.marginRight = 'auto'
button3.classList.add('void-settings-watermark-button')
// const keys3 = this.keybindingService.lookupKeybinding('workbench.action.openGlobalKeybindings');
// const button3 = append(recentsBox, $('button'));
// button3.textContent = `Void Settings`
// button3.style.display = 'block'
// button3.style.marginLeft = 'auto'
// button3.style.marginRight = 'auto'
// button3.classList.add('void-settings-watermark-button')
const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
if (keys3)
label3.set(keys3);
button3.onclick = () => {
this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
}
this.currentDisposables.add(label3);
// const label3 = new KeybindingLabel(button3, OS, { renderUnboundKeybindings: true, ...defaultKeybindingLabelStyles });
// if (keys3)
// label3.set(keys3);
// button3.onclick = () => {
// this.commandService.executeCommand(VOID_OPEN_SETTINGS_ACTION_ID)
// }
// this.currentDisposables.add(label3);
}
@@ -4,3 +4,23 @@
export const VOID_CTRL_L_ACTION_ID = 'void.ctrlLAction'
export const VOID_CTRL_K_ACTION_ID = 'void.ctrlKAction'
export const VOID_ACCEPT_DIFF_ACTION_ID = 'void.acceptDiff'
export const VOID_REJECT_DIFF_ACTION_ID = 'void.rejectDiff'
export const VOID_GOTO_NEXT_DIFF_ACTION_ID = 'void.goToNextDiff'
export const VOID_GOTO_PREV_DIFF_ACTION_ID = 'void.goToPrevDiff'
export const VOID_GOTO_NEXT_URI_ACTION_ID = 'void.goToNextUri'
export const VOID_GOTO_PREV_URI_ACTION_ID = 'void.goToPrevUri'
export const VOID_ACCEPT_FILE_ACTION_ID = 'void.acceptFile'
export const VOID_REJECT_FILE_ACTION_ID = 'void.rejectFile'
export const VOID_ACCEPT_ALL_DIFFS_ACTION_ID = 'void.acceptAllDiffs'
export const VOID_REJECT_ALL_DIFFS_ACTION_ID = 'void.rejectAllDiffs'
@@ -790,6 +790,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
console.log('starting autocomplete...', predictionType)
const featureName: FeatureName = 'Autocomplete'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -807,6 +808,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ
}),
modelSelection,
modelSelectionOptions,
overridesOfModel,
logging: { loggingName: 'Autocomplete' },
onText: () => { }, // unused in FIMMessage
// onText: async ({ fullText, newText }) => {
@@ -11,12 +11,12 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { URI } from '../../../../base/common/uri.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { ILLMMessageService } from '../common/sendLLMMessageService.js';
import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js';
import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { chat_userMessageContent, isABuiltinToolName } from '../common/prompt/prompts.js';
import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, ToolCallParams, ToolName, ToolResult } from '../common/toolsServiceTypes.js';
import { IToolsService } from './toolsService.js';
import { CancellationToken } from '../../../../base/common/cancellation.js';
import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js';
@@ -35,6 +35,10 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
import { timeout } from '../../../../base/common/async.js';
import { deepClone } from '../../../../base/common/objects.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IMCPService } from '../common/mcpService.js';
import { RawMCPToolCall } from '../common/mcpServiceTypes.js';
// related to retrying when LLM message has error
@@ -100,6 +104,13 @@ const defaultMessageState: UserMessageState = {
// a 'thread' means a chat message history
type WhenMounted = {
textAreaRef: { current: HTMLTextAreaElement | null }; // the textarea that this thread has, gets set in SidebarChat
scrollToBottom: () => void;
}
export type ThreadType = {
id: string; // store the id here too
createdAt: string; // ISO string
@@ -120,6 +131,15 @@ export type ThreadType = {
[codespanName: string]: CodespanLocationLink
}
}
mountedInfo?: {
whenMounted: Promise<WhenMounted>
_whenMountedResolver: (res: WhenMounted) => void
mountedIsResolvedRef: { current: boolean };
}
};
}
@@ -163,10 +183,11 @@ export type ThreadStreamState = {
llmInfo?: undefined;
toolInfo: {
toolName: ToolName;
toolParams: ToolCallParams[ToolName];
toolParams: ToolCallParams<ToolName>;
id: string;
content: string;
rawParams: RawToolParamsObj;
mcpServerName: string | undefined;
};
interrupt: Promise<() => void>;
} | {
@@ -180,7 +201,7 @@ export type ThreadStreamState = {
error?: undefined;
llmInfo?: undefined;
toolInfo?: undefined;
interrupt?: undefined;
interrupt: 'not_needed' | Promise<() => void>; // calling this should have no effect on state - would be too confusing. it just cancels the tool
}
}
@@ -235,6 +256,7 @@ export interface IChatThreadService {
isCurrentlyFocusingMessage(): boolean;
setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void;
popStagingSelections(numPops?: number): void;
addNewStagingSelection(newSelection: StagingSelectionItem): void;
dangerousSetState: (newState: ThreadsState) => void;
@@ -266,6 +288,9 @@ export interface IChatThreadService {
// jump to history
jumpToCheckpointBeforeMessageIdx(opts: { threadId: string, messageIdx: number, jumpToUserModified: boolean }): void;
focusCurrentChat: () => Promise<void>
blurCurrentChat: () => Promise<void>
}
export const IChatThreadService = createDecorator<IChatThreadService>('voidChatThreadService');
@@ -299,6 +324,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@INotificationService private readonly _notificationService: INotificationService,
@IConvertToLLMMessageService private readonly _convertToLLMMessagesService: IConvertToLLMMessageService,
@IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService,
@IDirectoryStrService private readonly _directoryStringService: IDirectoryStrService,
@IFileService private readonly _fileService: IFileService,
@IMCPService private readonly _mcpService: IMCPService,
) {
super()
this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state
@@ -332,6 +360,26 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
async focusCurrentChat() {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
const s = await thread.state.mountedInfo?.whenMounted
if (!this.isCurrentlyFocusingMessage()) {
s?.textAreaRef.current?.focus()
}
}
async blurCurrentChat() {
const threadId = this.state.currentThreadId
const thread = this.state.allThreads[threadId]
if (!thread) return
const s = await thread.state.mountedInfo?.whenMounted
if (!this.isCurrentlyFocusingMessage()) {
s?.textAreaRef.current?.blur()
}
}
dangerousSetState = (newState: ThreadsState) => {
this.state = newState
@@ -376,7 +424,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// this should be the only place this.state = ... appears besides constructor
private _setState(state: Partial<ThreadsState>, affectsCurrent: boolean) {
private _setState(state: Partial<ThreadsState>, doNotRefreshMountInfo?: boolean) {
const newState = {
...this.state,
...state
@@ -384,8 +432,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this.state = newState
if (affectsCurrent)
this._onDidChangeCurrentThread.fire()
this._onDidChangeCurrentThread.fire()
// if we just switched to a thread, update its current stream state if it's not streaming to possibly streaming
@@ -402,11 +449,33 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// if running now but stream state doesn't indicate it (happens if restart Void), cancel that last tool
if (lastMessage && lastMessage.role === 'tool' && lastMessage.type === 'running_now') {
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params })
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', content: lastMessage.content, id: lastMessage.id, rawParams: lastMessage.rawParams, result: null, name: lastMessage.name, params: lastMessage.params, mcpServerName: lastMessage.mcpServerName })
}
}
// if we did not just set the state to true, set mount info
if (doNotRefreshMountInfo) return
let whenMountedResolver: (w: WhenMounted) => void
const whenMountedPromise = new Promise<WhenMounted>((res) => whenMountedResolver = res)
this._setThreadState(threadId, {
mountedInfo: {
whenMounted: whenMountedPromise,
mountedIsResolvedRef: { current: false },
_whenMountedResolver: (w: WhenMounted) => {
whenMountedResolver(w)
const mountInfo = this.state.allThreads[threadId]?.state.mountedInfo
if (mountInfo) mountInfo.mountedIsResolvedRef.current = true
},
}
}, true) // do not trigger an update
}
@@ -468,51 +537,63 @@ class ChatThreadService extends Disposable implements IChatThreadService {
const lastMsg = thread.messages[thread.messages.length - 1]
let params: ToolCallParams[ToolName]
let params: ToolCallParams<ToolName>
if (lastMsg.role === 'tool' && lastMsg.type !== 'invalid_params') {
params = lastMsg.params
}
else return
const { name, id, rawParams } = lastMsg
const { name, id, rawParams, mcpServerName } = lastMsg
const errorMessage = this.errMsgs.rejected
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams })
const errorMessage = this.toolErrMsgs.rejected
this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams, mcpServerName })
this._setStreamState(threadId, undefined)
}
private _computeMCPServerOfToolName = (toolName: string) => {
return this._mcpService.getMCPTools()?.find(t => t.name === toolName)?.mcpServerName
}
async abortRunning(threadId: string) {
const thread = this.state.allThreads[threadId]
if (!thread) return // should never happen
// interrupt any effects
const interrupt = await this.streamState[threadId]?.interrupt
interrupt?.()
// add assistant message
if (this.streamState[threadId]?.isRunning === 'LLM') {
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._addUserCheckpoint({ threadId })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
}
// add tool that's running
else if (this.streamState[threadId]?.isRunning === 'tool') {
const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null })
const { toolName, toolParams, id, content: content_, rawParams, mcpServerName } = this.streamState[threadId].toolInfo
const content = content_ || this.toolErrMsgs.interrupted
this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null, mcpServerName })
}
// reject the tool for the user if relevant
else if (this.streamState[threadId]?.isRunning === 'awaiting_user') {
this.rejectLatestToolRequest(threadId)
}
else if (this.streamState[threadId]?.isRunning === 'idle') {
// do nothing
}
this._addUserCheckpoint({ threadId })
// interrupt any effects
const interrupt = await this.streamState[threadId]?.interrupt
if (typeof interrupt === 'function')
interrupt()
this._setStreamState(threadId, undefined)
}
private readonly errMsgs = {
private readonly toolErrMsgs = {
rejected: 'Tool call was rejected by the user.',
interrupted: 'Tool call was interrupted by the user.',
errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}`
}
@@ -525,36 +606,46 @@ class ChatThreadService extends Disposable implements IChatThreadService {
threadId: string,
toolName: ToolName,
toolId: string,
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams[ToolName] } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
mcpServerName: string | undefined,
opts: { preapproved: true, unvalidatedToolParams: RawToolParamsObj, validatedParams: ToolCallParams<ToolName> } | { preapproved: false, unvalidatedToolParams: RawToolParamsObj },
): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => {
// compute these below
let toolParams: ToolCallParams[ToolName]
let toolResult: Awaited<ToolResultType[typeof toolName]>
let toolParams: ToolCallParams<ToolName>
let toolResult: ToolResult<ToolName>
let toolResultStr: string
// Check if it's a built-in tool
const isBuiltInTool = isABuiltinToolName(toolName)
if (!opts.preapproved) { // skip this if pre-approved
// 1. validate tool params
try {
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
toolParams = params
} catch (error) {
if (isBuiltInTool) {
const params = this._toolsService.validateParams[toolName](opts.unvalidatedToolParams)
toolParams = params
}
else {
toolParams = opts.unvalidatedToolParams
}
}
catch (error) {
const errorMessage = getErrorMessage(error)
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, })
this._addMessageToThread(threadId, { role: 'tool', type: 'invalid_params', rawParams: opts.unvalidatedToolParams, result: null, name: toolName, content: errorMessage, id: toolId, mcpServerName })
return {}
}
// once validated, add checkpoint for edit
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) }
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['edit_file']).uri }) }
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as BuiltinToolCallParams['rewrite_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval
const approvalType = approvalTypeOfToolName[toolName]
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools'
if (approvalType) {
const autoApprove = this._settingsService.state.globalSettings.autoApprove[approvalType]
// add a tool_request because we use it for UI if a tool is loading (this should be improved in the future)
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams })
this._addMessageToThread(threadId, { role: 'tool', type: 'tool_request', content: '(Awaiting user permission...)', result: null, name: toolName, params: toolParams, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
if (!autoApprove) {
return { awaitingUserApproval: true }
}
@@ -566,9 +657,12 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 3. call the tool
// this._setStreamState(threadId, { isRunning: 'tool' }, 'merge')
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams } as const
const runningTool = { role: 'tool', type: 'running_now', name: toolName, params: toolParams, content: '(value not received yet...)', result: null, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName } as const
this._updateLatestTool(threadId, runningTool)
@@ -578,13 +672,28 @@ class ChatThreadService extends Disposable implements IChatThreadService {
try {
// set stream state
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams } })
this._setStreamState(threadId, { isRunning: 'tool', interrupt: interruptorPromise, toolInfo: { toolName, toolParams, id: toolId, content: 'interrupted...', rawParams: opts.unvalidatedToolParams, mcpServerName } })
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
const interruptor = () => { interrupted = true; interruptTool?.() }
resolveInterruptor(interruptor)
if (isBuiltInTool) {
const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any)
const interruptor = () => { interrupted = true; interruptTool?.() }
resolveInterruptor(interruptor)
toolResult = await result
toolResult = await result
}
else {
const mcpTools = this._mcpService.getMCPTools()
const mcpTool = mcpTools?.find(t => t.name === toolName)
if (!mcpTool) { throw new Error(`MCP tool ${toolName} not found`) }
resolveInterruptor(() => { })
toolResult = (await this._mcpService.callMCPTool({
serverName: mcpTool.mcpServerName ?? 'unknown_mcp_server',
toolName: toolName,
params: toolParams
})).result
}
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
}
@@ -593,24 +702,27 @@ class ChatThreadService extends Disposable implements IChatThreadService {
if (interrupted) { return { interrupted: true } } // the tool result is added where we interrupt, not here
const errorMessage = getErrorMessage(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
return {}
}
finally {
this._setStreamState(threadId, undefined)
}
// 4. stringify the result to give to the LLM
try {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
if (isBuiltInTool) {
toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any)
}
// For MCP tools, handle the result based on its type
else {
toolResultStr = this._mcpService.stringifyResult(toolResult as RawMCPToolCall)
}
} catch (error) {
const errorMessage = this.errMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams })
const errorMessage = this.toolErrMsgs.errWhenStringifying(error)
this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
return {}
}
// 5. add to history and keep going
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams })
this._updateLatestTool(threadId, { role: 'tool', type: 'success', params: toolParams, result: toolResult, name: toolName, content: toolResultStr, id: toolId, rawParams: opts.unvalidatedToolParams, mcpServerName })
return {}
};
@@ -631,11 +743,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}) {
let interruptedWhenIdle = false
const idleInterruptor = Promise.resolve(() => { interruptedWhenIdle = true })
// _runToolCall does not need setStreamState({idle}) before it, but it needs it after it. (handles its own setStreamState)
// above just defines helpers, below starts the actual function
const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here
// not running at start, clear state
this._setStreamState(threadId, { isRunning: 'idle' })
const { overridesOfModel } = this._settingsService.state
let nMessagesSent = 0
let shouldSendAnotherMessage = true
@@ -643,10 +757,15 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// before enter loop, call tool
if (callThisToolFirst) {
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
this._setStreamState(threadId, undefined)
if (interrupted) { return }
const { interrupted } = await this._runToolCall(threadId, callThisToolFirst.name, callThisToolFirst.id, callThisToolFirst.mcpServerName, { preapproved: true, unvalidatedToolParams: callThisToolFirst.rawParams, validatedParams: callThisToolFirst.params })
if (interrupted) {
this._setStreamState(threadId, undefined)
this._addUserCheckpoint({ threadId })
}
}
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
// tool use loop
while (shouldSendAnotherMessage) {
@@ -655,6 +774,8 @@ class ChatThreadService extends Disposable implements IChatThreadService {
isRunningWhenEnd = undefined
nMessagesSent += 1
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
const chatMessages = this.state.allThreads[threadId]?.messages ?? []
const { messages, separateSystemMessage } = await this._convertToLLMMessagesService.prepareLLMChatMessages({
chatMessages,
@@ -662,63 +783,46 @@ class ChatThreadService extends Disposable implements IChatThreadService {
chatMode
})
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
let shouldRetryLLM = true
let nAttempts = 0
while (shouldRetryLLM) {
if (this.streamState[threadId]?.isRunning) {
// if already streaming, stop
console.log('returning...', this.streamState[threadId])
return
}
shouldRetryLLM = false
nAttempts += 1
let resMessageIsDonePromise: (toolCall?: RawToolCallObj | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<RawToolCallObj | undefined>((res, rej) => { resMessageIsDonePromise = res })
type ResTypes =
| { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } }
| { type: 'llmError', error?: { message: string; fullError: Error | null; } }
| { type: 'llmAborted' }
let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<ResTypes>((res, rej) => { resMessageIsDonePromise = res })
let aborted = false
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
chatMode,
messages: messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } },
separateSystemMessage: separateSystemMessage,
onText: ({ fullText, fullReasoning, toolCall }) => {
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) })
},
onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => {
this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning })
this._setStreamState(threadId, undefined)
resMessageIsDonePromise(toolCall) // resolve with tool calls
resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls
},
onError: async (error) => {
if (this.streamState[threadId]?.isRunning !== 'LLM') {
console.log('Unexpected onError when', this.streamState[threadId]?.isRunning)
return
}
if (nAttempts < CHAT_RETRIES) {
nAttempts += 1
shouldRetryLLM = true
this._setStreamState(threadId, undefined) // clear later so can be interrupted
resMessageIsDonePromise()
}
else {
// add assistant's message to chat history, and clear selection
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name })
this._setStreamState(threadId, { isRunning: undefined, error })
resMessageIsDonePromise()
}
resMessageIsDonePromise({ type: 'llmError', error: error })
},
onAbort: () => {
// stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it)
aborted = true
this._setStreamState(threadId, { isRunning: 'idle' })
resMessageIsDonePromise()
resMessageIsDonePromise({ type: 'llmAborted' })
this._metricsService.capture('Agent Loop Done (Aborted)', { nMessagesSent, chatMode })
},
})
@@ -730,31 +834,67 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: '', reasoningSoFar: '', toolCallSoFar: null }, interrupt: Promise.resolve(() => this._llmMessageService.abort(llmCancelToken)) })
const toolCall = await messageIsDonePromise // wait for message to complete
const llmRes = await messageIsDonePromise // wait for message to complete
if (aborted) {
// if something else started running in the meantime
if (this.streamState[threadId]?.isRunning !== 'LLM') {
// console.log('Chat thread interrupted by a newer chat thread', this.streamState[threadId]?.isRunning)
return
}
// llm res aborted
if (llmRes.type === 'llmAborted') {
this._setStreamState(threadId, undefined)
return
}
if (shouldRetryLLM) {
this._setStreamState(threadId, { isRunning: 'idle' })
await timeout(RETRY_DELAY)
continue
// llm res error
else if (llmRes.type === 'llmError') {
// error, should retry
if (nAttempts < CHAT_RETRIES) {
shouldRetryLLM = true
this._setStreamState(threadId, { isRunning: 'idle', interrupt: idleInterruptor })
await timeout(RETRY_DELAY)
if (interruptedWhenIdle) {
this._setStreamState(threadId, undefined)
return
}
else
continue // retry
}
// error, but too many attempts
else {
const { error } = llmRes
const { displayContentSoFar, reasoningSoFar, toolCallSoFar } = this.streamState[threadId].llmInfo
this._addMessageToThread(threadId, { role: 'assistant', displayContent: displayContentSoFar, reasoning: reasoningSoFar, anthropicReasoning: null })
if (toolCallSoFar) this._addMessageToThread(threadId, { role: 'interrupted_streaming_tool', name: toolCallSoFar.name, mcpServerName: this._computeMCPServerOfToolName(toolCallSoFar.name) })
this._setStreamState(threadId, { isRunning: undefined, error })
this._addUserCheckpoint({ threadId })
return
}
}
this._setStreamState(threadId, { isRunning: 'idle' })
// llm res success
const { toolCall, info } = llmRes
this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning })
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity
// call tool if there is one
if (toolCall) {
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
const mcpTools = this._mcpService.getMCPTools()
const mcpTool = mcpTools?.find(t => t.name === toolCall.name)
const { awaitingUserApproval, interrupted } = await this._runToolCall(threadId, toolCall.name, toolCall.id, mcpTool?.mcpServerName, { preapproved: false, unvalidatedToolParams: toolCall.rawParams })
if (interrupted) {
this._setStreamState(threadId, undefined)
return
}
this._setStreamState(threadId, { isRunning: 'idle' })
if (awaitingUserApproval) { isRunningWhenEnd = 'awaiting_user' }
else { shouldSendAnotherMessage = true }
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
}
} // end while (attempts)
@@ -764,8 +904,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
this._setStreamState(threadId, { isRunning: isRunningWhenEnd })
// add checkpoint before the next user message
if (!isRunningWhenEnd)
this._addUserCheckpoint({ threadId })
if (!isRunningWhenEnd) this._addUserCheckpoint({ threadId })
// capture number of messages sent
this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode })
@@ -801,7 +940,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
}
@@ -1069,7 +1208,10 @@ We only need to do it for files that were edited since `from`, ie files between
class: undefined,
run: () => {
this.switchToThread(threadId)
// TODO!!! scroll to bottom
// scroll to bottom
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
m.scrollToBottom()
})
}
}]
},
@@ -1095,7 +1237,6 @@ We only need to do it for files that were edited since `from`, ie files between
// interrupt existing stream
if (this.streamState[threadId]?.isRunning) {
console.log('stopping....')
await this.abortRunning(threadId)
}
@@ -1104,14 +1245,12 @@ We only need to do it for files that were edited since `from`, ie files between
this._addUserCheckpoint({ threadId })
}
const { chatMode } = this._settingsService.state.globalSettings
// add user's message to chat history
const instructions = userMessage
const currSelns: StagingSelectionItem[] = _chatSelections ?? thread.state.stagingSelections
const opts = chatMode !== 'normal' ? { type: 'references' } as const : { type: 'fullCode', voidModelService: this._voidModelService } as const
const userMessageContent = await chat_userMessageContent(instructions, currSelns, opts) // user message + names of files (NOT content)
const userMessageContent = await chat_userMessageContent(instructions, currSelns, { directoryStrService: this._directoryStringService, fileService: this._fileService }) // user message + names of files (NOT content)
const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState }
this._addMessageToThread(threadId, userHistoryElt)
@@ -1121,6 +1260,11 @@ We only need to do it for files that were edited since `from`, ie files between
this._runChatAgent({ threadId, ...this._currentModelSelectionProps(), }),
threadId,
)
// scroll to bottom
this.state.allThreads[threadId]?.state.mountedInfo?.whenMounted.then(m => {
m.scrollToBottom()
})
}
@@ -1143,7 +1287,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
};
this._storeAllThreads(newThreads);
this._setState({ allThreads: newThreads }, true);
this._setState({ allThreads: newThreads });
}
// Now call the original method to add the user message and stream the response
@@ -1173,7 +1317,7 @@ We only need to do it for files that were edited since `from`, ie files between
messages: slicedMessages
}
}
}, true)
})
// re-add the message and stream it
this._addUserMessageAndStreamResponse({ userMessage, _chatSelections: currSelns, threadId })
@@ -1202,7 +1346,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
// URIs of files that have been read
else if (m.role === 'tool' && m.type === 'success' && m.name === 'read_file') {
const params = m.params as ToolCallParams['read_file']
const params = m.params as BuiltinToolCallParams['read_file']
addURI(params.uri)
}
}
@@ -1446,7 +1590,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}, true)
})
}
@@ -1477,7 +1621,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
switchToThread(threadId: string) {
this._setState({ currentThreadId: threadId }, true)
this._setState({ currentThreadId: threadId })
}
@@ -1500,7 +1644,7 @@ We only need to do it for files that were edited since `from`, ie files between
[newThread.id]: newThread
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id }, true)
this._setState({ allThreads: newThreads, currentThreadId: newThread.id })
}
@@ -1513,7 +1657,7 @@ We only need to do it for files that were edited since `from`, ie files between
// store the updated threads
this._storeAllThreads(newThreads);
this._setState({ ...this.state, allThreads: newThreads }, true)
this._setState({ ...this.state, allThreads: newThreads })
}
duplicateThread(threadId: string) {
@@ -1529,7 +1673,7 @@ We only need to do it for files that were edited since `from`, ie files between
[newThread.id]: newThread,
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true)
this._setState({ allThreads: newThreads })
}
@@ -1550,7 +1694,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
this._storeAllThreads(newThreads)
this._setState({ allThreads: newThreads }, true) // the current thread just changed (it had a message added to it)
this._setState({ allThreads: newThreads }) // the current thread just changed (it had a message added to it)
}
// sets the currently selected message (must be undefined if no message is selected)
@@ -1571,7 +1715,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}
}, true)
})
// // when change focused message idx, jump - do not jump back when click edit, too confusing.
// if (messageIdx !== undefined)
@@ -1611,6 +1755,31 @@ We only need to do it for files that were edited since `from`, ie files between
}
// Pops the staging selections from the current thread's state
popStagingSelections(numPops: number): void {
numPops = numPops ?? 1;
const focusedMessageIdx = this.getCurrentFocusedMessageIdx()
// set the selections to the proper value
let selections: StagingSelectionItem[] = []
let setSelections = (s: StagingSelectionItem[]) => { }
if (focusedMessageIdx === undefined) {
selections = this.getCurrentThreadState().stagingSelections
setSelections = (s: StagingSelectionItem[]) => this.setCurrentThreadState({ stagingSelections: s })
} else {
selections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections
setSelections = (s) => this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s })
}
setSelections([
...selections.slice(0, selections.length - numPops)
])
}
// set message.state
private _setCurrentMessageState(state: Partial<UserMessageState>, messageIdx: number): void {
@@ -1634,12 +1803,12 @@ We only need to do it for files that were edited since `from`, ie files between
)
}
}
}, true)
})
}
// set thread.state
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>): void {
private _setThreadState(threadId: string, state: Partial<ThreadType['state']>, doNotRefreshMountInfo?: boolean): void {
const thread = this.state.allThreads[threadId]
if (!thread) return
@@ -1654,7 +1823,7 @@ We only need to do it for files that were edited since `from`, ie files between
}
}
}
}, true)
}, doNotRefreshMountInfo)
}
@@ -6,17 +6,20 @@ import { createDecorator } from '../../../../platform/instantiation/common/insta
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js';
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
import { reParsedToolXMLString, chat_systemMessage } from '../common/prompt/prompts.js';
import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js';
import { IVoidSettingsService } from '../common/voidSettingsService.js';
import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from './directoryStrService.js';
import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { ITerminalToolService } from './terminalToolService.js';
import { IVoidModelService } from '../common/voidModelService.js';
import { URI } from '../../../../base/common/uri.js';
import { EndOfLinePreference } from '../../../../editor/common/model.js';
import { ToolName } from '../common/toolsServiceTypes.js';
import { IMCPService } from '../common/mcpService.js';
export const EMPTY_MESSAGE = '(empty message)'
@@ -37,11 +40,8 @@ type SimpleLLMMessage = {
const EMPTY_MESSAGE = '(empty message)'
const CHARS_PER_TOKEN = 4
const TRIM_TO_LEN = 60
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
const TRIM_TO_LEN = 120
@@ -69,7 +69,7 @@ openai on developer system message - https://cdn.openai.com/spec/model-spec-2024
*/
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => {
const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): AnthropicOrOpenAILLMMessage[] => {
const newMessages: OpenAILLMChatMessage[] = [];
@@ -136,8 +136,9 @@ assistant: ...content, call(name, id, params)
user: ...content, result(id, content)
*/
type AnthropicOrOpenAILLMMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages;
for (let i = 0; i < messages.length; i += 1) {
@@ -195,9 +196,9 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA
}
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => {
const llmChatMessages: LLMChatMessage[] = [];
const llmChatMessages: AnthropicOrOpenAILLMMessage[] = [];
for (let i = 0; i < messages.length; i += 1) {
const c = messages[i]
@@ -206,7 +207,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
if (c.role === 'assistant') {
// if called a tool (message after it), re-add its XML to the message
// alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere
let content: LLMChatMessage['content'] = c.content
let content: AnthropicOrOpenAILLMMessage['content'] = c.content
if (next?.role === 'tool') {
content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}`
}
@@ -238,33 +239,17 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
}
const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => {
const llmChatMessages: LLMChatMessage[] = []
if (!specialToolFormat) { // XML tool behavior
return prepareMessages_XML_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'anthropic-style') {
return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning)
}
else if (specialToolFormat === 'openai-style') {
return prepareMessages_openai_tools(messages)
}
return llmChatMessages
}
// --- CHAT ---
const prepareMessages = ({
messages,
const prepareOpenAIOrAnthropicMessages = ({
messages: messages_,
systemMessage,
aiInstructions,
supportsSystemMessage,
specialToolFormat,
supportsAnthropicReasoning,
contextWindow,
maxOutputTokens,
reservedOutputTokenSpace,
}: {
messages: SimpleLLMMessage[],
systemMessage: string,
@@ -273,20 +258,35 @@ const prepareMessages = ({
specialToolFormat: 'openai-style' | 'anthropic-style' | undefined,
supportsAnthropicReasoning: boolean,
contextWindow: number,
maxOutputTokens: number | null | undefined,
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096
reservedOutputTokenSpace: number | null | undefined,
}): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => {
reservedOutputTokenSpace = Math.max(
contextWindow * 1 / 2, // reserve at least 1/4 of the token window length
reservedOutputTokenSpace ?? 4_096 // defaults to 4096
)
let messages: (SimpleLLMMessage | { role: 'system', content: string })[] = deepClone(messages_)
// ================ system message ================
// A COMPLETE HACK: last message is system message for context purposes
const sysMsgParts: string[] = []
if (aiInstructions) sysMsgParts.push(`GUIDELINES (from the user's .voidrules file):\n${aiInstructions}`)
if (systemMessage) sysMsgParts.push(systemMessage)
const combinedSystemMessage = sysMsgParts.join('\n\n')
messages.unshift({ role: 'system', content: combinedSystemMessage })
// ================ trim ================
messages = deepClone(messages)
messages = messages.map(m => ({ ...m, content: m.role !== 'tool' ? m.content.trim() : m.content }))
type MesType = (typeof messages)[0]
// ================ fit into context ================
// the higher the weight, the higher the desire to truncate - TRIM HIGHEST WEIGHT MESSAGES
const alreadyTrimmedIdxes = new Set<number>()
const weight = (message: SimpleLLMMessage, messages: SimpleLLMMessage[], idx: number) => {
const weight = (message: MesType, messages: MesType[], idx: number) => {
const base = message.content.length
let multiplier: number
@@ -294,22 +294,30 @@ const prepareMessages = ({
if (message.role === 'user') {
multiplier *= 1
}
else if (message.role === 'system') {
multiplier *= .01 // very low weight
}
else {
multiplier *= 10 // llm tokens are far less valuable than user tokens
}
// 1st message, last 3 msgs, any already modified message should be low in weight
if (idx === 0 || idx >= messages.length - 1 - 3 || alreadyTrimmedIdxes.has(idx)) {
// any already modified message should not be trimmed again
if (alreadyTrimmedIdxes.has(idx)) {
multiplier = 0
}
// 1st and last messages should be very low weight
if (idx <= 1 || idx >= messages.length - 1 - 3) {
multiplier *= .05
}
return base * multiplier
}
const _findLargestByWeight = (messages: SimpleLLMMessage[]) => {
const _findLargestByWeight = (messages_: MesType[]) => {
let largestIndex = -1
let largestWeight = -Infinity
for (let i = 0; i < messages.length; i += 1) {
const m = messages[i]
const w = weight(m, messages, i)
const w = weight(m, messages_, i)
if (w > largestWeight) {
largestWeight = w
largestIndex = i
@@ -320,7 +328,11 @@ const prepareMessages = ({
let totalLen = 0
for (const m of messages) { totalLen += m.content.length }
const charsNeedToTrim = totalLen - (contextWindow - maxOutputTokens) * CHARS_PER_TOKEN
const charsNeedToTrim = totalLen - Math.max(
(contextWindow - reservedOutputTokenSpace) * CHARS_PER_TOKEN, // can be 0, in which case charsNeedToTrim=everything, bad
5_000 // ensure we don't trim at least 5k chars (just a random small value)
)
// <----------------------------------------->
// 0 | | |
@@ -340,41 +352,54 @@ const prepareMessages = ({
// if can finish here, do
const numCharsWillTrim = m.content.length - TRIM_TO_LEN
if (numCharsWillTrim > remainingCharsToTrim) {
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim).trim()
// trim remainingCharsToTrim + '...'.length chars
m.content = m.content.slice(0, m.content.length - remainingCharsToTrim - '...'.length).trim() + '...'
break
}
remainingCharsToTrim -= numCharsWillTrim
m.content = m.content.substring(0, TRIM_TO_LEN - 3) + '...'
m.content = m.content.substring(0, TRIM_TO_LEN - '...'.length) + '...'
alreadyTrimmedIdxes.add(trimIdx)
}
// ================ system message hack ================
const newSysMsg = messages.shift()!.content
// ================ tools and anthropicReasoning ================
const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning)
// SYSTEM MESSAGE HACK: we shifted (removed) the system message role, so now SimpleLLMMessage[] is valid
// ================ system message concat ================
let llmChatMessages: AnthropicOrOpenAILLMMessage[] = []
if (!specialToolFormat) { // XML tool behavior
llmChatMessages = prepareMessages_XML_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
}
else if (specialToolFormat === 'anthropic-style') {
llmChatMessages = prepareMessages_anthropic_tools(messages as SimpleLLMMessage[], supportsAnthropicReasoning)
}
else if (specialToolFormat === 'openai-style') {
llmChatMessages = prepareMessages_openai_tools(messages as SimpleLLMMessage[])
}
const llmMessages = llmChatMessages
// find system messages and concatenate them
const newSystemMessage = aiInstructions ?
`${(systemMessage ? `${systemMessage}\n\n` : '')}GUIDELINES\n${aiInstructions}`
: systemMessage
// ================ system message add as first llmMessage ================
let separateSystemMessageStr: string | undefined = undefined
// if supports system message
if (supportsSystemMessage) {
if (supportsSystemMessage === 'separated')
separateSystemMessageStr = newSystemMessage
separateSystemMessageStr = newSysMsg
else if (supportsSystemMessage === 'system-role')
llmMessages.unshift({ role: 'system', content: newSystemMessage }) // add new first message
llmMessages.unshift({ role: 'system', content: newSysMsg }) // add new first message
else if (supportsSystemMessage === 'developer-role')
llmMessages.unshift({ role: 'developer', content: newSystemMessage }) // add new first message
llmMessages.unshift({ role: 'developer', content: newSysMsg }) // add new first message
}
// if does not support system message
else {
const newFirstMessage = {
role: 'user',
content: `<SYSTEM_MESSAGE>\n${newSystemMessage}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
content: `<SYSTEM_MESSAGE>\n${newSysMsg}\n</SYSTEM_MESSAGE>\n${llmMessages[0].content}`
} as const
llmMessages.splice(0, 1) // delete first message
llmMessages.unshift(newFirstMessage) // add new first message
@@ -382,14 +407,25 @@ const prepareMessages = ({
// ================ no empty message ================
for (const currMsg of llmMessages) {
for (let i = 0; i < llmMessages.length; i += 1) {
const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i]
const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1]
if (currMsg.role === 'tool') continue
// if content is a string, replace string with empty msg
if (typeof currMsg.content === 'string')
if (typeof currMsg.content === 'string') {
currMsg.content = currMsg.content || EMPTY_MESSAGE
}
else {
// if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry
// allowed to be empty if has a tool in it or following it
if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) {
currMsg.content = currMsg.content.filter(c => !(c.type === 'text' && !c.text)) as any
continue
}
if (nextMsg?.role === 'tool') continue
// replace any empty text entries with empty msg, and make sure there's at least 1 entry
for (const c of currMsg.content) {
if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE
}
@@ -406,9 +442,79 @@ const prepareMessages = ({
type GeminiUserPart = (GeminiLLMChatMessage & { role: 'user' })['parts'][0]
type GeminiModelPart = (GeminiLLMChatMessage & { role: 'model' })['parts'][0]
const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
let latestToolName: ToolName | undefined = undefined
const messages2: GeminiLLMChatMessage[] = messages.map((m): GeminiLLMChatMessage | null => {
if (m.role === 'assistant') {
if (typeof m.content === 'string') {
return { role: 'model', parts: [{ text: m.content }] }
}
else {
const parts: GeminiModelPart[] = m.content.map((c): GeminiModelPart | null => {
if (c.type === 'text') {
return { text: c.text }
}
else if (c.type === 'tool_use') {
latestToolName = c.name
return { functionCall: { id: c.id, name: c.name, args: c.input } }
}
else return null
}).filter(m => !!m)
return { role: 'model', parts, }
}
}
else if (m.role === 'user') {
if (typeof m.content === 'string') {
return { role: 'user', parts: [{ text: m.content }] } satisfies GeminiLLMChatMessage
}
else {
const parts: GeminiUserPart[] = m.content.map((c): GeminiUserPart | null => {
if (c.type === 'text') {
return { text: c.text }
}
else if (c.type === 'tool_result') {
if (!latestToolName) return null
return { functionResponse: { id: c.tool_use_id, name: latestToolName, response: { output: c.content } } }
}
else return null
}).filter(m => !!m)
return { role: 'user', parts, }
}
}
else return null
}).filter(m => !!m)
return messages2
}
const prepareMessages = (params: {
messages: SimpleLLMMessage[],
systemMessage: string,
aiInstructions: string,
supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated',
specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined,
supportsAnthropicReasoning: boolean,
contextWindow: number,
reservedOutputTokenSpace: number | null | undefined,
providerName: ProviderName
}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => {
const specialFormat = params.specialToolFormat // this is just for ts stupidness
// if need to convert to gemini style of messaes, do that (treat as anthropic style, then convert to gemini style)
if (params.providerName === 'gemini' || specialFormat === 'gemini-style') {
const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined })
const messages = res.messages as AnthropicLLMChatMessage[]
const messages2 = prepareGeminiMessages(messages)
return { messages: messages2, separateSystemMessage: res.separateSystemMessage }
}
return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat })
}
@@ -418,7 +524,6 @@ export interface IConvertToLLMMessageService {
prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined }
prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }>
prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] }
}
export const IConvertToLLMMessageService = createDecorator<IConvertToLLMMessageService>('ConvertToLLMMessageService');
@@ -435,6 +540,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
@ITerminalToolService private readonly terminalToolService: ITerminalToolService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@IVoidModelService private readonly voidModelService: IVoidModelService,
@IMCPService private readonly mcpService: IMCPService,
) {
super()
}
@@ -470,7 +576,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
// system message
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => {
private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined) => {
const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath)
const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || [];
@@ -481,10 +587,13 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
`...Directories string cut off, use tools to read more...`
: `...Directories string cut off, ask user for more if necessary...`
})
const includeXMLToolDefinitions = !specialToolFormat
const mcpTools = this.mcpService.getMCPTools()
const persistentTerminalIDs = this.terminalToolService.listPersistentTerminalIds()
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, includeXMLToolDefinitions })
const systemMessage = chat_systemMessage({ workspaceFolders, openedURIs, directoryStr, activeURI, persistentTerminalIDs, chatMode, mcpTools, includeXMLToolDefinitions })
return systemMessage
}
@@ -527,20 +636,23 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
prepareLLMSimpleMessages: IConvertToLLMMessageService['prepareLLMSimpleMessages'] = ({ simpleMessages, systemMessage, modelSelection, featureName }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { overridesOfModel } = this.voidSettingsService.state
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName)
} = getModelCapabilities(providerName, modelName, overridesOfModel)
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName]
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel)
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
const { messages, separateSystemMessage } = prepareMessages({
messages: simpleMessages,
@@ -550,27 +662,33 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
maxOutputTokens,
reservedOutputTokenSpace,
providerName,
})
return { messages, separateSystemMessage };
}
prepareLLMChatMessages: IConvertToLLMMessageService['prepareLLMChatMessages'] = async ({ chatMessages, chatMode, modelSelection }) => {
if (modelSelection === null) return { messages: [], separateSystemMessage: undefined }
const { overridesOfModel } = this.voidSettingsService.state
const { providerName, modelName } = modelSelection
const {
specialToolFormat,
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName)
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
} = getModelCapabilities(providerName, modelName, overridesOfModel)
const { disableSystemMessage } = this.voidSettingsService.state.globalSettings;
const fullSystemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
const systemMessage = disableSystemMessage ? '' : fullSystemMessage;
const modelSelectionOptions = this.voidSettingsService.state.optionsOfModelSelection['Chat'][modelSelection.providerName]?.[modelSelection.modelName]
// Get combined AI instructions
const aiInstructions = this._getCombinedAIInstructions();
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions)
const maxOutputTokens = getMaxOutputTokens(providerName, modelName, { isReasoningEnabled })
const isReasoningEnabled = getIsReasoningEnabledState('Chat', providerName, modelName, modelSelectionOptions, overridesOfModel)
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
const { messages, separateSystemMessage } = prepareMessages({
@@ -581,7 +699,8 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
specialToolFormat,
supportsAnthropicReasoning: providerName === 'anthropic',
contextWindow,
maxOutputTokens,
reservedOutputTokenSpace,
providerName,
})
return { messages, separateSystemMessage };
}
@@ -14,8 +14,6 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit
import { findDiffs } from './helpers/findDiffs.js';
import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { registerColor } from '../../../../platform/theme/common/colorUtils.js';
import { Color, RGBA } from '../../../../base/common/color.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js';
import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js';
@@ -25,7 +23,10 @@ import * as dom from '../../../../base/browser/dom.js';
import { Widget } from '../../../../base/browser/ui/widget.js';
import { URI } from '../../../../base/common/uri.js';
import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, } from '../common/prompt/prompts.js';
import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, tripleTick, } from '../common/prompt/prompts.js';
import { IVoidCommandBarService } from './voidCommandBarService.js';
import { IKeybindingService } from '../../../../platform/keybinding/common/keybinding.js';
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID } from './actionIDs.js';
import { mountCtrlK } from './react/out/quick-edit-tsx/index.js'
import { QuickEditPropsType } from './quickEditActions.js';
@@ -48,27 +49,6 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js';
// import { isMacintosh } from '../../../../base/common/platform.js';
// import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
}
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
const greenBG = new Color(new RGBA(155, 185, 85, .2)); // default is RGBA(155, 185, 85, .2)
registerColor('void.greenBG', configOfBG(greenBG), '', true);
const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2)
registerColor('void.redBG', configOfBG(redBG), '', true);
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
const numLinesOfStr = (str: string) => str.split('\n').length
@@ -129,29 +109,42 @@ const removeWhitespaceExceptNewlines = (str: string): string => {
// finds block.orig in fileContents and return its range in file
// startingAtLine is 1-indexed and inclusive
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => {
// returns 1-indexed lines
const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' }) => {
const startLineIdx = (fileContents: string) => startingAtLine !== undefined ?
fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine
const returnAns = (fileContents: string, idx: number) => {
const startLine = numLinesOfStr(fileContents.substring(0, idx + 1))
const numLines = numLinesOfStr(text)
const endLine = startLine + numLines - 1
return [startLine, endLine] as const
}
const startingAtLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ?
fileContents.split('\n').slice(0, opts.startingAtLine).join('\n').length // num characters in all lines before startingAtLine
: 0
// idx = starting index in fileContents
let idx = fileContents.indexOf(text, startLineIdx(fileContents))
let idx = fileContents.indexOf(text, startingAtLineIdx(fileContents))
// if idx was found
if (idx !== -1) {
return returnAns(fileContents, idx)
}
if (!canFallbackToRemoveWhitespace)
return 'Not found' as const
// try to find it ignoring all whitespace this time
if (idx === -1 && canFallbackToRemoveWhitespace) {
text = removeWhitespaceExceptNewlines(text)
fileContents = removeWhitespaceExceptNewlines(fileContents)
idx = fileContents.indexOf(text, startLineIdx(fileContents));
}
text = removeWhitespaceExceptNewlines(text)
fileContents = removeWhitespaceExceptNewlines(fileContents)
idx = fileContents.indexOf(text, startingAtLineIdx(fileContents));
if (idx === -1) return 'Not found' as const
const lastIdx = fileContents.lastIndexOf(text)
if (lastIdx !== idx) return 'Not unique' as const
const startLine = fileContents.substring(0, idx).split('\n').length
const numLines = numLinesOfStr(text)
const endLine = startLine + numLines - 1
return [startLine, endLine] as const
return returnAns(fileContents, idx)
}
@@ -276,7 +269,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
public processRawKeybindingText(keybindingStr: string): string {
return keybindingStr
.replace(/Enter/g, '↵') // ⏎
.replace(/Backspace/g, '⌫');
}
// private _notifyError = (e: Parameters<OnError>[0]) => {
// const details = errorDetails(e.fullError)
@@ -590,7 +587,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
else { throw new Error('Void 1') }
const buttonsWidget = new AcceptRejectInlineWidget({
const buttonsWidget = this._instantiationService.createInstance(AcceptRejectInlineWidget, {
editor,
onAccept: () => {
this.acceptDiff({ diffid })
@@ -1127,8 +1124,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
return
}
public async callBeforeStartApplying(opts: CallBeforeStartApplyingOpts) {
const uri = this._getURIBeforeStartApplying(opts)
public async callBeforeApplyOrEdit(givenURI: URI | 'current') {
const uri = this._uriOfGivenURI(givenURI)
if (!uri) return
await this._voidModelService.initializeModel(uri)
await this._voidModelService.saveModel(uri) // save the URI
@@ -1182,17 +1179,33 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
// auto accept
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
}
}
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
const onError = (e: { message: string; fullError: Error | null; }) => {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message)
}
try {
this._instantlyApplySRBlocks(uri, searchReplaceBlocks)
}
catch (e) {
onError({ message: e + '', fullError: null })
}
onDone()
}
public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) {
public instantlyRewriteFile({ uri, newContent }: { uri: URI, newContent: string }) {
// start diffzone
const res = this._startStreamingDiffZone({
uri,
@@ -1210,9 +1223,14 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
// auto accept
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
}
}
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false })
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: true })
onDone()
}
@@ -1335,6 +1353,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { from, } = opts
const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -1439,6 +1458,11 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
// auto accept
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
}
}
// throws
@@ -1446,7 +1470,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError
throw e.fullError || new Error(e.message)
}
const extractText = (fullText: string, recentlyAddedTextLen: number) => {
@@ -1486,6 +1510,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
@@ -1560,25 +1585,31 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => {
const descStr = str === `Not found` ?
`The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === `Not unique` ?
`The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: str === 'Has overlap' ?
`The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}`
: ``
// string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has
// const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n')
// const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : ''
// const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : ''
// const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}`
const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
const errMsg = `${descStr}\n${soFarStr}`
return errMsg
/**
* Generates a human-readable error message for an invalid ORIGINAL search block.
*/
private _errContentOfInvalidStr = (
str: 'Not found' | 'Not unique' | 'Has overlap',
blockOrig: string,
): string => {
const problematicCode = `${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}`
// use a switch for better readability / exhaustiveness check
let descStr: string
switch (str) {
case 'Not found':
descStr = `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.`
break
case 'Not unique':
descStr = `The edit was not applied. The text in ORIGINAL must be unique in the file being edited, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.`
break
case 'Has overlap':
descStr = `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.`
break
default:
descStr = ''
}
return descStr
}
@@ -1589,23 +1620,31 @@ class EditCodeService extends Disposable implements IEditCodeService {
const { model } = this._voidModelService.getModel(uri)
if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`)
const modelStr = model.getValue(EndOfLinePreference.LF)
// .split('\n').map(l => '\t' + l).join('\n') // for testing purposes only, remember to remove this
const modelStrLines = modelStr.split('\n')
const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = []
for (const b of blocks) {
const i = modelStr.indexOf(b.orig)
if (i === -1)
throw new Error(this._errContentOfInvalidStr('Not found', b.orig))
const j = modelStr.lastIndexOf(b.orig)
if (i !== j)
throw new Error(this._errContentOfInvalidStr('Not unique', b.orig))
const res = findTextInCode(b.orig, modelStr, true, { returnType: 'lines' })
if (typeof res === 'string')
throw new Error(this._errContentOfInvalidStr(res, b.orig))
let [startLine, endLine] = res
startLine -= 1 // 0-index
endLine -= 1
replacements.push({
origStart: i,
origEnd: i + b.orig.length - 1, // INCLUSIVE
block: b,
})
// including newline before start
const origStart = (startLine !== 0 ?
modelStrLines.slice(0, startLine).join('\n') + '\n'
: '').length
// including endline at end
const origEnd = modelStrLines.slice(0, endLine + 1).join('\n').length - 1
replacements.push({ origStart, origEnd, block: b });
}
// sort in increasing order
replacements.sort((a, b) => a.origStart - b.origStart)
@@ -1627,12 +1666,12 @@ class EditCodeService extends Disposable implements IEditCodeService {
'wholeFileRange',
{ shouldRealignDiffAreas: true }
)
}
private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise<void>] | undefined {
const { from, applyStr, } = opts
const featureName: FeatureName = 'Apply'
const overridesOfModel = this._settingsService.state.overridesOfModel
const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName]
const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[featureName][modelSelection.providerName]?.[modelSelection.modelName] : undefined
@@ -1708,13 +1747,18 @@ class EditCodeService extends Disposable implements IEditCodeService {
this._deleteTrackingZone(trackingZone)
onFinishEdit()
// auto accept
if (this._settingsService.state.globalSettings.autoAcceptLLMChanges) {
this.acceptOrRejectAllDiffAreas({ uri, removeCtrlKs: false, behavior: 'accept' })
}
}
const onError = (e: { message: string; fullError: Error | null; }) => {
// this._notifyError(e)
onDone()
this._undoHistory(uri)
throw e.fullError || new Error(e.message) // throw error h
throw e.fullError || new Error(e.message)
}
// refresh now in case onText takes a while to get 1st message
@@ -1768,7 +1812,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// update stream state to the first line of original if some portion of original has been written
if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) {
const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line
const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine)
const originalRange = findTextInCode(block.orig, originalFileCode, false, { startingAtLine, returnType: 'lines' })
if (typeof originalRange !== 'string') {
const [startLine, _] = convertOriginalRangeToFinalRange(originalRange)
diffZone._streamState.line = startLine
@@ -1794,7 +1838,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
// if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it
if (!(blockNum in addedTrackingZoneOfBlockNum)) {
const originalBounds = findTextInCode(block.orig, originalFileCode, true)
const originalBounds = findTextInCode(block.orig, originalFileCode, true, { returnType: 'lines' })
// if error
// Check for overlap with existing modified ranges
const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => {
@@ -1813,9 +1857,10 @@ class EditCodeService extends Disposable implements IEditCodeService {
console.log('block.orig:', block.orig)
console.log('---------')
const content = this._errContentOfInvalidStr(errorMessage, block.orig)
const retryMsg = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.'
messages.push(
{ role: 'assistant', content: fullText }, // latest output
{ role: 'user', content: content } // user explanation of what's wrong
{ role: 'user', content: content + '\n' + retryMsg } // user explanation of what's wrong
)
// REVERT ALL BLOCKS
@@ -1911,6 +1956,7 @@ class EditCodeService extends Disposable implements IEditCodeService {
messages,
modelSelection,
modelSelectionOptions,
overridesOfModel,
separateSystemMessage,
chatMode: null, // not chat
onText: (params) => {
@@ -1924,6 +1970,8 @@ class EditCodeService extends Disposable implements IEditCodeService {
if (blocks.length === 0) {
this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`)
}
this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true })
try {
this._instantlyApplySRBlocks(uri, fullText)
@@ -2231,31 +2279,77 @@ registerSingleton(IEditCodeService, EditCodeService, InstantiationType.Eager);
class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
public getId() { return this.ID }
public getDomNode() { return this._domNode; }
public getPosition() { return null }
public getId(): string {
return this.ID || ''; // Ensure we always return a string
}
public getDomNode(): HTMLElement {
return this._domNode;
}
public getPosition() {
return null;
}
private readonly _domNode: HTMLElement;
private readonly editor
private readonly ID
private readonly startLine
private readonly _domNode: HTMLElement; // Using the definite assignment assertion
private readonly editor: ICodeEditor;
private readonly ID: string;
private readonly startLine: number;
constructor({ editor, onAccept, onReject, diffid, startLine, offsetLines }: { editor: ICodeEditor; onAccept: () => void; onReject: () => void; diffid: string, startLine: number, offsetLines: number }) {
super()
constructor(
{ editor, onAccept, onReject, diffid, startLine, offsetLines }: {
editor: ICodeEditor;
onAccept: () => void;
onReject: () => void;
diffid: string,
startLine: number,
offsetLines: number
},
@IVoidCommandBarService private readonly _voidCommandBarService: IVoidCommandBarService,
@IKeybindingService private readonly _keybindingService: IKeybindingService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
) {
super();
this.ID = editor.getModel()?.uri.fsPath + diffid;
const uri = editor.getModel()?.uri;
// Initialize with default values
this.ID = ''
this.editor = editor;
this.startLine = startLine;
if (!uri) {
const { dummyDiv } = dom.h('div@dummyDiv');
this._domNode = dummyDiv
return;
}
this.ID = uri.fsPath + diffid;
const lineHeight = editor.getOption(EditorOption.lineHeight);
const getAcceptRejectText = () => {
const acceptKeybinding = this._keybindingService.lookupKeybinding(VOID_ACCEPT_DIFF_ACTION_ID);
const rejectKeybinding = this._keybindingService.lookupKeybinding(VOID_REJECT_DIFF_ACTION_ID);
// Use the standalone function directly since we're in a nested class that
// can't access EditCodeService's methods
const acceptKeybindLabel = this._editCodeService.processRawKeybindingText(acceptKeybinding && acceptKeybinding.getLabel() || '');
const rejectKeybindLabel = this._editCodeService.processRawKeybindingText(rejectKeybinding && rejectKeybinding.getLabel() || '');
const commandBarStateAtUri = this._voidCommandBarService.stateOfURI[uri.fsPath];
const selectedDiffIdx = commandBarStateAtUri?.diffIdx ?? 0; // 0th item is selected by default
const thisDiffIdx = commandBarStateAtUri?.sortedDiffIds.indexOf(diffid) ?? null;
const showLabel = thisDiffIdx === selectedDiffIdx
const acceptText = `Accept${showLabel ? ` ` + acceptKeybindLabel : ''}`;
const rejectText = `Reject${showLabel ? ` ` + rejectKeybindLabel : ''}`;
return { acceptText, rejectText }
}
const { acceptText, rejectText } = getAcceptRejectText()
// Create container div with buttons
const { acceptButton, rejectButton, buttons } = dom.h('div@buttons', [
dom.h('button@acceptButton', []),
@@ -2269,11 +2363,14 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
buttons.style.paddingRight = '4px';
buttons.style.zIndex = '1';
buttons.style.transform = `translateY(${offsetLines * lineHeight}px)`;
buttons.style.justifyContent = 'flex-end';
buttons.style.width = '100%';
buttons.style.pointerEvents = 'none';
// Style accept button
acceptButton.onclick = onAccept;
acceptButton.textContent = 'Accept';
acceptButton.textContent = acceptText;
acceptButton.style.backgroundColor = acceptBg;
acceptButton.style.border = acceptBorder;
acceptButton.style.color = buttonTextColor;
@@ -2287,10 +2384,12 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
acceptButton.style.cursor = 'pointer';
acceptButton.style.height = '100%';
acceptButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
acceptButton.style.pointerEvents = 'auto';
// Style reject button
rejectButton.onclick = onReject;
rejectButton.textContent = 'Reject';
rejectButton.textContent = rejectText;
rejectButton.style.backgroundColor = rejectBg;
rejectButton.style.border = rejectBorder;
rejectButton.style.color = buttonTextColor;
@@ -2304,6 +2403,7 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
rejectButton.style.cursor = 'pointer';
rejectButton.style.height = '100%';
rejectButton.style.boxShadow = '0 2px 3px rgba(0,0,0,0.2)';
rejectButton.style.pointerEvents = 'auto';
@@ -2324,16 +2424,28 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
}
// Mount first, then update positions
editor.addOverlayWidget(this);
updateTop()
updateLeft()
setTimeout(() => {
updateTop()
updateLeft()
}, 0)
this._register(editor.onDidScrollChange(e => { updateTop() }))
this._register(editor.onDidChangeModelContent(e => { updateTop() }))
this._register(editor.onDidLayoutChange(e => { updateTop(); updateLeft() }))
// Listen for state changes in the command bar service
this._register(this._voidCommandBarService.onDidChangeState(e => {
if (uri && e.uri.fsPath === uri.fsPath) {
const { acceptText, rejectText } = getAcceptRejectText()
acceptButton.textContent = acceptText;
rejectButton.textContent = rejectText;
}
}));
// mount this widget
editor.addOverlayWidget(this);
@@ -2341,8 +2453,8 @@ class AcceptRejectInlineWidget extends Widget implements IOverlayWidget {
}
public override dispose(): void {
this.editor.removeOverlayWidget(this)
super.dispose()
this.editor.removeOverlayWidget(this);
super.dispose();
}
}
@@ -42,10 +42,12 @@ export const IEditCodeService = createDecorator<IEditCodeService>('editCodeServi
export interface IEditCodeService {
readonly _serviceBrand: undefined;
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
processRawKeybindingText(keybindingStr: string): string;
callBeforeApplyOrEdit(uri: URI | 'current'): Promise<void>;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void;
instantlyApplyNewContent(opts: { uri: URI; newContent: string }): void;
instantlyRewriteFile(opts: { uri: URI; newContent: string }): void;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;
@@ -54,6 +56,8 @@ export interface IEditCodeService {
diffOfId: Record<string, Diff>;
acceptOrRejectAllDiffAreas(opts: { uri: URI, removeCtrlKs: boolean, behavior: 'reject' | 'accept', _addToHistory?: boolean }): void;
acceptDiff({ diffid }: { diffid: number }): void;
rejectDiff({ diffid }: { diffid: number }): void;
// events
onDidAddOrDeleteDiffZones: Event<{ uri: URI }>;
@@ -0,0 +1,329 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { VSBuffer } from '../../../../base/common/buffer.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { env } from '../../../../base/common/process.js';
import { URI } from '../../../../base/common/uri.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { TransferEditorType, TransferFilesInfo } from './extensionTransferTypes.js';
export interface IExtensionTransferService {
readonly _serviceBrand: undefined; // services need this, just leave it undefined
transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType): Promise<string | undefined>
deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null): Promise<void>
}
export const IExtensionTransferService = createDecorator<IExtensionTransferService>('ExtensionTransferService');
// Define extensions to skip when transferring
const extensionBlacklist = [
// ignore extensions
'ms-vscode-remote.remote', // ms-vscode-remote.remote-ssh, ms-vscode-remote.remote-wsl
'ms-vscode.remote', // ms-vscode.remote-explorer
// ignore other AI copilots that could conflict with Void keybindings
'sourcegraph.cody-ai',
'continue.continue',
'codeium.codeium',
'saoudrizwan.claude-dev', // cline
'rooveterinaryinc.roo-cline', // roo
'supermaven.supermaven' // supermaven
// 'github.copilot',
];
const isBlacklisted = (fsPath: string | undefined) => {
return extensionBlacklist.find(bItem => fsPath?.includes(bItem))
}
class ExtensionTransferService extends Disposable implements IExtensionTransferService {
_serviceBrand: undefined;
constructor(
@IFileService private readonly _fileService: IFileService,
) {
super()
}
async transferExtensions(os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType) {
const transferTheseFiles = transferTheseFilesOfOS(os, fromEditor)
const fileService = this._fileService
let errAcc = ''
for (const { from, to, isExtensions } of transferTheseFiles) {
// Check if the source file exists before attempting to copy
try {
if (!isExtensions) {
console.log('transferring item', from, to)
const exists = await fileService.exists(from)
if (exists) {
// Ensure the destination directory exists
const toParent = URI.joinPath(to, '..')
const toParentExists = await fileService.exists(toParent)
if (!toParentExists) {
await fileService.createFolder(toParent)
}
await fileService.copy(from, to, true)
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
}
// extensions folder
else {
console.log('transferring extensions...', from, to)
const exists = await fileService.exists(from)
if (exists) {
const stat = await fileService.resolve(from)
const toParent = URI.joinPath(to) // extensions/
const toParentExists = await fileService.exists(toParent)
if (!toParentExists) {
await fileService.createFolder(toParent)
}
for (const extensionFolder of stat.children ?? []) {
const from = extensionFolder.resource
const to = URI.joinPath(toParent, extensionFolder.name)
const toStat = await fileService.resolve(from)
if (toStat.isDirectory) {
if (!isBlacklisted(extensionFolder.resource.fsPath)) {
await fileService.copy(from, to, true)
}
}
else if (toStat.isFile) {
if (extensionFolder.name === 'extensions.json') {
try {
const contentsStr = await fileService.readFile(from)
const json: any = JSON.parse(contentsStr.value.toString())
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
const jsonStr = JSON.stringify(j2)
await fileService.writeFile(to, VSBuffer.fromString(jsonStr))
}
catch {
console.log('Error copying extensions.json, skipping')
}
}
}
}
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
console.log('done transferring extensions.')
}
}
catch (e) {
console.error('Error copying file:', e)
errAcc += `Error copying ${from.toString()}: ${e}\n`
}
}
if (errAcc) return errAcc
return undefined
}
async deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null) {
const fileService = this._fileService
const extensionsURI = getExtensionsFolder(os)
if (!extensionsURI) return
const eURI = await fileService.resolve(extensionsURI)
for (const child of eURI.children ?? []) {
try {
if (child.isDirectory) {
// if is blacklisted
if (isBlacklisted(child.resource.fsPath)) {
console.log('Deleting extension', child.resource.fsPath)
await fileService.del(child.resource, { recursive: true, useTrash: true })
}
}
else if (child.isFile) {
// if is extensions.json
if (child.name === 'extensions.json') {
console.log('Updating extensions.json', child.resource.fsPath)
try {
const contentsStr = await fileService.readFile(child.resource)
const json: any = JSON.parse(contentsStr.value.toString())
const j2 = json.filter((entry: { identifier?: { id?: string } }) => !isBlacklisted(entry?.identifier?.id))
const jsonStr = JSON.stringify(j2)
await fileService.writeFile(child.resource, VSBuffer.fromString(jsonStr))
}
catch {
console.log('Error copying extensions.json, skipping')
}
}
}
}
catch (e) {
console.error('Could not delete extension', child.resource.fsPath, e)
}
}
}
}
registerSingleton(IExtensionTransferService, ExtensionTransferService, InstantiationType.Eager); // lazily loaded, even if Eager
const transferTheseFilesOfOS = (os: 'mac' | 'windows' | 'linux' | null, fromEditor: TransferEditorType = 'VS Code'): TransferFilesInfo => {
if (os === null)
throw new Error(`One-click switch is not possible in this environment.`)
if (os === 'mac') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`$HOME not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, 'Library', 'Application Support', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
if (os === 'linux') {
const homeDir = env['HOME']
if (!homeDir) throw new Error(`variable for $HOME location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.config', 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), homeDir, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
if (os === 'windows') {
const appdata = env['APPDATA']
if (!appdata) throw new Error(`variable for %APPDATA% location not found`)
const userprofile = env['USERPROFILE']
if (!userprofile) throw new Error(`variable for %USERPROFILE% location not found`)
if (fromEditor === 'VS Code') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Code', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.vscode', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Cursor') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Cursor', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.cursor', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
} else if (fromEditor === 'Windsurf') {
return [{
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'settings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'settings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Windsurf', 'User', 'keybindings.json'),
to: URI.joinPath(URI.from({ scheme: 'file' }), appdata, 'Void', 'User', 'keybindings.json'),
}, {
from: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.windsurf', 'extensions'),
to: URI.joinPath(URI.from({ scheme: 'file' }), userprofile, '.void-editor', 'extensions'),
isExtensions: true,
}]
}
}
throw new Error(`os '${os}' not recognized or editor type '${fromEditor}' not supported for this OS`)
}
const getExtensionsFolder = (os: 'mac' | 'windows' | 'linux' | null) => {
const t = transferTheseFilesOfOS(os, 'VS Code') // from editor doesnt matter
return t.find(f => f.isExtensions)?.to
}
@@ -0,0 +1,11 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { URI } from '../../../../base/common/uri.js'
export type TransferEditorType = 'VS Code' | 'Cursor' | 'Windsurf'
// https://github.com/VSCodium/vscodium/blob/master/docs/index.md#migrating-from-visual-studio-code-to-vscodium
// https://code.visualstudio.com/docs/editor/extension-marketplace#_where-are-extensions-installed
export type TransferFilesInfo = { from: URI, to: URI, isExtensions?: boolean }[]
@@ -0,0 +1,78 @@
import { localize2 } from '../../../../nls.js';
import { URI } from '../../../../base/common/uri.js';
import { Action2, registerAction2, MenuId } from '../../../../platform/actions/common/actions.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { INotificationService } from '../../../../platform/notification/common/notification.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IClipboardService } from '../../../../platform/clipboard/common/clipboardService.js';
import { IDirectoryStrService } from '../common/directoryStrService.js';
import { messageOfSelection } from '../common/prompt/prompts.js';
import { IVoidModelService } from '../common/voidModelService.js';
class FilePromptActionService extends Action2 {
private static readonly VOID_COPY_FILE_PROMPT_ID = 'void.copyfileprompt'
constructor() {
super({
id: FilePromptActionService.VOID_COPY_FILE_PROMPT_ID,
title: localize2('voidCopyPrompt', 'Void: Copy Prompt'),
menu: [{
id: MenuId.ExplorerContext,
group: '8_void',
order: 1,
}]
});
}
async run(accessor: ServicesAccessor, uri: URI): Promise<void> {
try {
const fileService = accessor.get(IFileService);
const clipboardService = accessor.get(IClipboardService)
const directoryStrService = accessor.get(IDirectoryStrService)
const voidModelService = accessor.get(IVoidModelService)
const stat = await fileService.stat(uri)
const folderOpts = {
maxChildren: 1000,
maxCharsPerFile: 2_000_000,
} as const
let m: string = 'No contents detected'
if (stat.isFile) {
m = await messageOfSelection({
type: 'File',
uri,
language: (await voidModelService.getModelSafe(uri)).model?.getLanguageId() || '',
state: { wasAddedAsCurrentFile: false, },
}, {
folderOpts,
directoryStrService,
fileService,
})
}
if (stat.isDirectory) {
m = await messageOfSelection({
type: 'Folder',
uri,
}, {
folderOpts,
fileService,
directoryStrService,
})
}
await clipboardService.writeText(m)
} catch (error) {
const notificationService = accessor.get(INotificationService)
notificationService.error(error + '')
}
}
}
registerAction2(FilePromptActionService)
@@ -0,0 +1,49 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Disposable } from '../../../../base/common/lifecycle.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
import { IExtensionTransferService } from './extensionTransferService.js';
import { os } from '../common/helpers/systemInfo.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { timeout } from '../../../../base/common/async.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
// Onboarding contribution that mounts the component at startup
export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContribution {
static readonly ID = 'workbench.contrib.voidMiscWorkbenchContribs';
constructor(
@IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.initialize();
}
private initialize(): void {
// delete blacklisted extensions once (this is for people who already installed them)
const deleteExtensionsStorageId = 'void-deleted-blacklist-2'
const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
if (!alreadyDeleted) {
this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
this.extensionTransferService.deleteBlacklistExtensions(os)
}
// after some time, trigger a resize event for the blank screen error
timeout(5_000).then(() => {
// Get the active window reference for multi-window support
const targetWindow = getActiveWindow();
// Trigger a window resize event to ensure proper layout calculations
targetWindow.dispatchEvent(new Event('resize'))
})
}
}
registerWorkbenchContribution2(MiscWorkbenchContribs.ID, MiscWorkbenchContribs, WorkbenchPhase.Eventually);
@@ -13,7 +13,7 @@ import { roundRangeToLines } from './sidebarActions.js';
import { VOID_CTRL_K_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
import { IMetricsService } from '../common/metricsService.js';
import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js';
export type QuickEditPropsType = {
diffareaid: number,
@@ -42,6 +42,7 @@ registerAction2(class extends Action2 {
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyK,
weight: KeybindingWeight.VoidExtension,
when: ContextKeyExpr.deserialize('editorFocus && !terminalFocus'),
}
});
}
@@ -3,14 +3,14 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useState, useEffect, useCallback } from 'react'
import { useAccessor, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { useState, useEffect, useCallback, useRef, Fragment } from 'react'
import { useAccessor, useChatThreadsState, useChatThreadsStreamState, useCommandBarState, useCommandBarURIListener, useSettingsState } from '../util/services.js'
import { usePromise, useRefState } from '../util/helpers.js'
import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { FileSymlink, LucideIcon, RotateCw, Terminal } from 'lucide-react'
import { Check, X, Square, Copy, Play, } from 'lucide-react'
import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { getBasename, ListableToolItem, voidOpenFileFn, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js'
import { PlacesType, VariantType } from 'react-tooltip'
enum CopyButtonText {
@@ -24,8 +24,9 @@ type IconButtonProps = {
Icon: LucideIcon
}
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: IconButtonProps & React.ButtonHTMLAttributes<HTMLButtonElement>) => {
return <button
disabled={disabled}
onClick={(e) => {
e.preventDefault();
@@ -34,19 +35,19 @@ export const IconShell1 = ({ onClick, Icon, disabled, className, ...props }: Ico
}}
// border border-void-border-1 rounded
className={`
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
size-[18px]
p-[2px]
flex items-center justify-center
text-sm text-void-fg-3
hover:brightness-110
disabled:opacity-50 disabled:cursor-not-allowed
${className}
`}
{...props}
>
<Icon />
</button>
)
}
// export const IconShell2 = ({ onClick, title, Icon, disabled, className }: IconButtonProps) => (
@@ -108,7 +109,7 @@ export const JumpToFileButton = ({ uri, ...props }: { uri: URI | 'current' } & R
<IconShell1
Icon={FileSymlink}
onClick={() => {
commandService.executeCommand('vscode.open', uri, { preview: true })
voidOpenFileFn(uri, accessor)
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Go to file' })}
{...props}
@@ -131,48 +132,41 @@ export const JumpToTerminalButton = ({ onClick }: { onClick: () => void }) => {
// state persisted for duration of react only
// TODO change this to use type `ChatThreads.applyBoxState[applyBoxId]`
const applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
const _applyingURIOfApplyBoxIdRef: { current: { [applyBoxId: string]: URI | undefined } } = { current: {} }
const getUriBeingApplied = (applyBoxId: string) => {
return applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
return _applyingURIOfApplyBoxIdRef.current[applyBoxId] ?? null
}
export const useApplyButtonState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' }) => {
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
export const useApplyStreamState = ({ applyBoxId }: { applyBoxId: string }) => {
const accessor = useAccessor()
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const [_, rerender] = useState(0)
const getStreamState = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return 'idle-no-changes'
return voidCommandBarService.getStreamState(uri)
}, [voidCommandBarService, applyBoxId])
const [currStreamStateRef, setStreamState] = useRefState(getStreamState())
const setApplying = useCallback((uri: URI | undefined) => {
_applyingURIOfApplyBoxIdRef.current[applyBoxId] = uri ?? undefined
setStreamState(getStreamState())
}, [setStreamState, getStreamState, applyBoxId])
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = (
getUriBeingApplied(applyBoxId)?.fsPath === uri_.fsPath
|| (uri !== 'current' && uri.fsPath === uri_.fsPath)
)
if (shouldUpdate) {
rerender(c => c + 1)
const uri = getUriBeingApplied(applyBoxId)
if (uri?.fsPath === uri_.fsPath) {
setStreamState(getStreamState())
}
}, [applyBoxId, uri]))
const currStreamState = getStreamState()
}, [setStreamState, applyBoxId, getStreamState]))
return {
getStreamState,
isDisabled,
currStreamState,
}
return { currStreamStateRef, setApplying }
}
@@ -202,10 +196,24 @@ const tooltipPropsForApplyBlock = ({ tooltipName, color = undefined, position =
'data-tooltip-offset': offset,
})
export const useEditToolStreamState = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI }) => {
const accessor = useAccessor()
const voidCommandBarService = accessor.get('IVoidCommandBarService')
const [streamState, setStreamState] = useState(voidCommandBarService.getStreamState(uri))
// listen for stream updates on this box
useCommandBarURIListener(useCallback((uri_) => {
const shouldUpdate = uri.fsPath === uri_.fsPath
if (shouldUpdate) { setStreamState(voidCommandBarService.getStreamState(uri)) }
}, [voidCommandBarService, applyBoxId, uri]))
return { streamState, }
}
export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: string, uri: URI | 'current' } & React.HTMLAttributes<HTMLDivElement>) => {
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
const currStreamState = currStreamStateRef.current
const color = (
currStreamState === 'idle-no-changes' ? 'dark' :
@@ -231,82 +239,255 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
}
export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => {
const terminalLanguages = new Set([
'bash',
'shellscript',
'shell',
'powershell',
'bat',
'zsh',
'sh',
'fish',
'nushell',
'ksh',
'xonsh',
'elvish',
])
const ApplyButtonsForTerminal = ({
codeStr,
applyBoxId,
uri,
language,
}: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const accessor = useAccessor()
const metricsService = accessor.get('IMetricsService')
const terminalToolService = accessor.get('ITerminalToolService')
const settingsState = useSettingsState()
const [isShellRunning, setIsShellRunning] = useState<boolean>(false)
const interruptToolRef = useRef<(() => void) | null>(null)
const isDisabled = isShellRunning
const onClickSubmit = useCallback(async () => {
if (isShellRunning) return
try {
setIsShellRunning(true)
const terminalId = await terminalToolService.createPersistentTerminal({ cwd: null })
const { interrupt } = await terminalToolService.runCommand(
codeStr,
{ type: 'persistent', persistentTerminalId: terminalId }
);
interruptToolRef.current = interrupt
metricsService.capture('Execute Shell', { length: codeStr.length })
} catch (e) {
setIsShellRunning(false)
console.error('Failed to execute in terminal:', e)
}
}, [codeStr, uri, applyBoxId, metricsService, terminalToolService, isShellRunning])
if (isShellRunning) {
return (
<IconShell1
Icon={X}
onClick={() => {
interruptToolRef.current?.();
setIsShellRunning(false);
}}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
);
}
if (isDisabled) {
return null
}
return <IconShell1
Icon={Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
const ApplyButtonsForEdit = ({
codeStr,
applyBoxId,
uri,
language,
}: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const notificationService = accessor.get('INotificationService')
const {
currStreamState,
isDisabled,
getStreamState,
} = useApplyButtonState({ applyBoxId, uri })
const settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId })
const onClickSubmit = useCallback(async () => {
if (isDisabled) return
if (getStreamState() === 'streaming') return
const opts = {
if (currStreamStateRef.current === 'streaming') return
await editCodeService.callBeforeApplyOrEdit(uri)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying({
from: 'ClickApply',
applyStr: codeStr,
uri: uri,
startBehavior: 'reject-conflicts',
} as const
}) ?? []
setApplying(newApplyingUri)
await editCodeService.callBeforeStartApplying(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
if (!applyDonePromise) {
notificationService.info(`Void Error: We couldn't run Apply here. ${uri === 'current' ? 'This Apply block wants to run on the current file, but you might not have a file open.' : `This Apply block wants to run on ${uri.fsPath}, but it might not exist.`}`)
}
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.interruptURIStreaming({ uri: uri })
notificationService.info(`Void Error: There was a problem running Apply: ${e}.`)
})
applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined
// rerender(c => c + 1)
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [isDisabled, getStreamState, editCodeService, codeStr, uri, applyBoxId, metricsService])
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService, notificationService])
const onInterrupt = useCallback(() => {
if (getStreamState() !== 'streaming') return
const onClickStop = useCallback(() => {
if (currStreamStateRef.current !== 'streaming') return
const uri = getUriBeingApplied(applyBoxId)
if (!uri) return
editCodeService.interruptURIStreaming({ uri })
metricsService.capture('Stop Apply', {})
}, [getStreamState, applyBoxId, editCodeService, metricsService])
}, [currStreamStateRef, applyBoxId, editCodeService, metricsService])
const onAccept = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'accept', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const onReject = useCallback(() => {
const uri = getUriBeingApplied(applyBoxId)
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [applyBoxId, editCodeService])
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const currStreamState = currStreamStateRef.current
if (currStreamState === 'streaming') {
return <IconShell1
Icon={Square}
onClick={onInterrupt}
onClick={onClickStop}
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
}
if (isDisabled) {
return null
}
if (currStreamState === 'idle-no-changes') {
return <IconShell1
Icon={Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
if (currStreamState === 'idle-has-changes') {
return <Fragment>
<IconShell1
Icon={X}
onClick={onReject}
{...tooltipPropsForApplyBlock({ tooltipName: 'Remove' })}
/>
<IconShell1
Icon={Check}
onClick={onAccept}
{...tooltipPropsForApplyBlock({ tooltipName: 'Keep' })}
/>
</Fragment>
}
}
export const ApplyButtonsHTML = (params: {
codeStr: string,
applyBoxId: string,
language?: string,
uri: URI | 'current';
}) => {
const { language } = params
const isShellLanguage = !!language && terminalLanguages.has(language)
if (isShellLanguage) {
return <ApplyButtonsForTerminal {...params} />
}
else {
return <ApplyButtonsForEdit {...params} />
}
}
export const EditToolAcceptRejectButtonsHTML = ({
codeStr,
applyBoxId,
uri,
type,
threadId,
}: {
codeStr: string,
applyBoxId: string,
} & ({
uri: URI,
type: 'edit_file' | 'rewrite_file',
threadId: string,
})
) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const metricsService = accessor.get('IMetricsService')
const { streamState } = useEditToolStreamState({ applyBoxId, uri })
const settingsState = useSettingsState()
const chatThreadsStreamState = useChatThreadsStreamState(threadId)
const isRunning = chatThreadsStreamState?.isRunning
const isDisabled = !!isFeatureNameDisabled('Chat', settingsState) || !applyBoxId
const onAccept = useCallback(() => {
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const onReject = useCallback(() => {
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
if (isDisabled) return null
if (streamState === 'idle-no-changes') {
return null
}
if (streamState === 'idle-has-changes') {
if (isRunning === 'LLM' || isRunning === 'tool') return null
return <>
<IconShell1
Icon={X}
@@ -325,13 +506,13 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string
export const BlockCodeApplyWrapper = ({
children,
initValue,
codeStr,
applyBoxId,
language,
canApply,
uri,
}: {
initValue: string;
codeStr: string;
children: React.ReactNode;
applyBoxId: string;
canApply: boolean;
@@ -340,7 +521,8 @@ export const BlockCodeApplyWrapper = ({
}) => {
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const { currStreamState } = useApplyButtonState({ applyBoxId, uri })
const { currStreamStateRef } = useApplyStreamState({ applyBoxId })
const currStreamState = currStreamStateRef.current
const name = uri !== 'current' ?
@@ -348,7 +530,7 @@ export const BlockCodeApplyWrapper = ({
name={<span className='not-italic'>{getBasename(uri.fsPath)}</span>}
isSmall={true}
showDot={false}
onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }}
onClick={() => { voidOpenFileFn(uri, accessor) }}
/>
: <span>{language}</span>
@@ -364,8 +546,8 @@ export const BlockCodeApplyWrapper = ({
</div>
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} language={language} />
</div>
</div>
@@ -9,12 +9,12 @@ import { marked, MarkedToken, Token } from 'marked'
import { convertToVscodeLang, detectLanguage } from '../../../../common/helpers/languageHelpers.js'
import { BlockCodeApplyWrapper } from './ApplyBlockHoverButtons.js'
import { useAccessor } from '../util/services.js'
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'
import { URI } from '../../../../../../../base/common/uri.js'
import { isAbsolute } from '../../../../../../../base/common/path.js'
import { separateOutFirstLine } from '../../../../common/helpers/util.js'
import { BlockCode } from '../util/inputs.js'
import { CodespanLocationLink } from '../../../../common/chatThreadServiceTypes.js'
import { getBasename, getRelative, voidOpenFileFn } from '../sidebar-tsx/SidebarChat.js'
export type ChatMessageLocation = {
@@ -89,13 +89,18 @@ const LatexRender = ({ latex }: { latex: string }) => {
// }
}
const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => {
const Codespan = ({ text, className, onClick, tooltip }: { text: string, className?: string, onClick?: () => void, tooltip?: string }) => {
// TODO compute this once for efficiency. we should use `labels.ts/shorten` to display duplicates properly
return <code
className={`font-mono font-medium rounded-sm bg-void-bg-1 px-1 ${className}`}
onClick={onClick}
{...tooltip ? {
'data-tooltip-id': 'void-tooltip',
'data-tooltip-content': tooltip,
'data-tooltip-place': 'top',
} : {}}
>
{text}
</code>
@@ -115,8 +120,11 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
const [didComputeCodespanLink, setDidComputeCodespanLink] = useState<boolean>(false)
let link: CodespanLocationLink | undefined = undefined
if (rawText.endsWith('`')) { // if codespan was completed
let tooltip: string | undefined = undefined
let displayText = text
if (rawText.endsWith('`')) {
// get link from cache
link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId })
@@ -127,41 +135,33 @@ const CodespanWithLink = ({ text, rawText, chatMessageLocation }: { text: string
chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId })
setDidComputeCodespanLink(true) // rerender
})
}
if (link?.displayText) {
displayText = link.displayText
}
if (isValidUri(displayText)) {
tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip
displayText = getBasename(displayText)
}
}
const onClick = () => {
if (!link) return;
const selection = link.selection
// open the file
commandService.executeCommand('vscode.open', link.uri).then(() => {
// select the text
setTimeout(() => {
if (!selection) return;
const editor = editorService.getActiveCodeEditor()
if (!editor) return;
editor.setSelection(selection)
editor.revealRange(selection, ScrollType.Immediate)
}, 50) // needed when document was just opened and needs to initialize
})
// Use the updated voidOpenFileFn to open the file and handle selection
if (link.selection)
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
else
voidOpenFileFn(link.uri, accessor);
}
return <Codespan
// text={link?.displayText || text}
text={link?.displayText || text}
text={displayText}
onClick={onClick}
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
tooltip={tooltip || undefined}
/>
}
@@ -319,12 +319,12 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, ..
return <BlockCodeApplyWrapper
canApply={isCodeblockClosed}
applyBoxId={applyBoxId}
initValue={contents}
codeStr={contents}
language={language}
uri={uri || 'current'}
>
<BlockCode
initValue={contents}
initValue={contents.trimEnd()} // \n\n adds a permanent newline which creates a flash
language={language}
/>
</BlockCodeApplyWrapper>
@@ -4,7 +4,7 @@
*--------------------------------------------------------------------------------------*/
import React, { useEffect, useState } from 'react'
import { useIsDark, useSidebarState } from '../util/services.js'
import { useIsDark } from '../util/services.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { QuickEditChat } from './QuickEditChat.js'
import { QuickEditPropsType } from '../../../quickEditActions.js'
@@ -71,7 +71,7 @@ export const QuickEditChat = ({
startBehavior: 'keep-conflicts',
} as const
await editCodeService.callBeforeStartApplying(opts)
await editCodeService.callBeforeApplyOrEdit(opts)
const [newApplyingUri, applyDonePromise] = editCodeService.startApplying(opts) ?? []
// catch any errors by interrupting the stream
applyDonePromise?.catch(e => { if (newApplyingUri) editCodeService.interruptCtrlKStreaming({ diffareaid }) })
@@ -3,7 +3,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { useIsDark, useSidebarState } from '../util/services.js';
import { useIsDark } from '../util/services.js';
// import { SidebarThreadSelector } from './SidebarThreadSelector.js';
// import { SidebarChat } from './SidebarChat.js';
@@ -12,8 +12,6 @@ import { SidebarChat } from './SidebarChat.js';
import ErrorBoundary from './ErrorBoundary.js';
export const Sidebar = ({ className }: { className: string }) => {
const sidebarState = useSidebarState()
const { currentTab: tab } = sidebarState
const isDark = useIsDark()
return <div
@@ -29,34 +27,12 @@ export const Sidebar = ({ className }: { className: string }) => {
`}
>
{/* <span onClick={() => {
const tabs = ['chat', 'settings', 'threadSelector']
const index = tabs.indexOf(tab)
sidebarStateService.setState({ currentTab: tabs[(index + 1) % tabs.length] as any })
}}>clickme {tab}</span> */}
{/* <div className={`w-full h-auto mb-2 ${isHistoryOpen ? '' : 'hidden'} ring-2 ring-widget-shadow z-10`}>
<ErrorBoundary>
<SidebarThreadSelector />
</ErrorBoundary>
</div> */}
<div className={`w-full h-full ${tab === 'chat' ? '' : 'hidden'}`}>
<div className={`w-full h-full`}>
<ErrorBoundary>
<SidebarChat />
</ErrorBoundary>
{/* <ErrorBoundary>
<ModelSelectionSettings />
</ErrorBoundary> */}
</div>
{/* <div className={`w-full h-full ${tab === 'settings' ? '' : 'hidden'}`}>
<ErrorBoundary>
<VoidProviderSettings />
</ErrorBoundary>
</div> */}
</div>
</div>
File diff suppressed because it is too large Load Diff
@@ -11,138 +11,6 @@ import { Check, Copy, Icon, LoaderCircle, MessageCircleQuestion, Trash2, UserChe
import { IsRunningType, ThreadType } from '../../../chatThreadService.js';
export const OldSidebarThreadSelector = () => {
const accessor = useAccessor()
const sidebarStateService = accessor.get('ISidebarStateService')
return (
<div className="flex p-2 flex-col gap-y-1 max-h-[200px] overflow-y-auto">
<div className="w-full relative flex justify-center items-center">
{/* title */}
<h2 className='font-bold text-lg'>{`History`}</h2>
{/* X button at top right */}
<button
type='button'
className='absolute top-0 right-0'
onClick={() => sidebarStateService.setState({ isHistoryOpen: false })}
>
<IconX
size={16}
className="p-[1px] stroke-[2] opacity-80 text-void-fg-3 hover:brightness-95"
/>
</button>
</div>
{/* a list of all the past threads */}
{/* <OldPastThreadsList /> */}
</div>
)
}
const truncate = (s: string) => {
let len = s.length
const TRUNC_AFTER = 16
if (len >= TRUNC_AFTER)
s = s.substring(0, TRUNC_AFTER) + '...'
return s
}
const OldPastThreadsList = () => {
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
const threadsState = useChatThreadsState()
const { allThreads } = threadsState
// sorted by most recent to least recent
const sortedThreadIds = Object.keys(allThreads ?? {})
.sort((threadId1, threadId2) => (allThreads[threadId1]?.lastModified ?? 0) > (allThreads[threadId2]?.lastModified ?? 0) ? -1 : 1)
.filter(threadId => (allThreads![threadId]?.messages.length ?? 0) !== 0)
return <div className="px-1">
<ul className="flex flex-col gap-y-0.5 overflow-y-auto list-disc">
{sortedThreadIds.length === 0
? <div key="nothreads" className="text-center text-void-fg-3 brightness-90 text-root">{`There are no chat threads yet.`}</div>
: sortedThreadIds.map((threadId) => {
if (!allThreads) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
const pastThread = allThreads[threadId];
if (!pastThread) {
return <li key="error" className="text-void-warning">{`Error accessing chat history.`}</li>;
}
let firstMsg = null;
// let secondMsg = null;
const firstUserMsgIdx = pastThread.messages.findIndex((msg) => msg.role === 'user');
if (firstUserMsgIdx !== -1) {
// firstMsg = truncate(pastThread.messages[firstMsgIdx].displayContent ?? '');
const firsUsertMsgObj = pastThread.messages[firstUserMsgIdx]
firstMsg = firsUsertMsgObj.role === 'user' && firsUsertMsgObj.displayContent || '';
} else {
firstMsg = '""';
}
// const secondMsgIdx = pastThread.messages.findIndex(
// (msg, i) => msg.role !== 'system' && !!msg.displayContent && i > firstMsgIdx
// );
// if (secondMsgIdx !== -1) {
// secondMsg = truncate(pastThread.messages[secondMsgIdx].displayContent ?? '');
// }
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
return (
<li key={pastThread.id}>
<button
type='button'
className={`
hover:bg-void-bg-1
${threadsState.currentThreadId === pastThread.id ? 'bg-void-bg-1' : ''}
rounded-sm px-2 py-1
w-full
text-left
flex items-center
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false })
}}
title={new Date(pastThread.lastModified).toLocaleString()}
>
<div className='truncate'>{`${firstMsg}`}</div>
<div>{`\u00A0(${numMessages})`}</div>
</button>
</li>
);
})
}
</ul>
</div>
}
const numInitialThreads = 3
export const PastThreadsList = ({ className = '' }: { className?: string }) => {
@@ -313,7 +181,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
const accessor = useAccessor()
const chatThreadsService = accessor.get('IChatThreadService')
const sidebarStateService = accessor.get('ISidebarStateService')
// const settingsState = useSettingsState()
// const convertService = accessor.get('IConvertToLLMMessageService')
@@ -353,13 +220,14 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
const numMessages = pastThread.messages.filter((msg) => msg.role === 'assistant' || msg.role === 'user').length;
const detailsHTML = <span
className='gap-1 inline-flex items-center'
// data-tooltip-id='void-tooltip'
// data-tooltip-content={`Last modified ${formatTime(new Date(pastThread.lastModified))}`}
// data-tooltip-place='top'
>
{/* <span>{numMessages}</span> */}
<span className='opacity-60'>{numMessages}</span>
{` `}
{formatDate(new Date(pastThread.lastModified))}
{/* {` messages `} */}
</span>
return <div
@@ -369,7 +237,6 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
`}
onClick={() => {
chatThreadsService.switchToThread(pastThread.id);
sidebarStateService.setState({ isHistoryOpen: false });
}}
onMouseEnter={() => setHoveredIdx(idx)}
onMouseLeave={() => setHoveredIdx(null)}
@@ -377,15 +244,19 @@ const PastThreadElement = ({ pastThread, idx, hoveredIdx, setHoveredIdx, isRunni
<div className="flex items-center justify-between gap-1">
<span className="flex items-center gap-2 min-w-0 overflow-hidden">
{/* spinner */}
{isRunning === 'LLM' || isRunning === 'tool' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
{isRunning === 'LLM' || isRunning === 'tool' || isRunning === 'idle' ? <LoaderCircle className="animate-spin bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
isRunning === 'awaiting_user' ? <MessageCircleQuestion className="bg-void-stroke-1 flex-shrink-0 flex-grow-0" size={14} />
:
null}
{/* name */}
<span className="truncate overflow-hidden text-ellipsis">{firstMsg}</span>
<span className="truncate overflow-hidden text-ellipsis"
data-tooltip-id='void-tooltip'
data-tooltip-content={numMessages + ' messages'}
data-tooltip-place='top'
>{firstMsg}</span>
<span className='opacity-60'>{`(${numMessages})`}</span>
{/* <span className='opacity-60'>{`(${numMessages})`}</span> */}
</span>
<div className="flex items-center gap-x-1 opacity-60">
@@ -9,10 +9,13 @@ type ReturnType<T> = [
// use this if state might be too slow to catch
export const useRefState = <T,>(initVal: T): ReturnType<T> => {
const [_, _setState] = useState(false)
// this actually makes a difference being an int, not a boolean.
// if it's a boolean and changes happen to fast, it goes with old values and leads to *very* weird bugs (like returning JSX, but not actually rendering it)
const [_s, _setState] = useState(0)
const ref = useRef<T>(initVal)
const setState = useCallback((newVal: T) => {
_setState(n => !n) // call rerender
_setState(n => n + 1) // call rerender
ref.current = newVal
}, [])
return [ref, setState]
@@ -17,9 +17,14 @@ import { asCssVariable } from '../../../../../../../platform/theme/common/colorU
import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js';
import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react';
import { URI } from '../../../../../../../base/common/uri.js';
import { getBasename } from '../sidebar-tsx/SidebarChat.js';
import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js';
import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react';
import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js';
import { DiffEditorWidget } from '../../../../../../../editor/browser/widget/diffEditor/diffEditorWidget.js';
import { extractSearchReplaceBlocks, ExtractedSearchReplaceBlock } from '../../../../common/helpers/extractCodeFromResult.js';
import { IAccessibilitySignalService } from '../../../../../../../platform/accessibilitySignal/browser/accessibilitySignalService.js';
import { IEditorProgressService } from '../../../../../../../platform/progress/common/progress.js';
import { detectLanguage } from '../../../../common/helpers/languageHelpers.js';
// type guard
@@ -55,12 +60,13 @@ export const WidgetComponent = <CtorParams extends any[], Instance>({ ctor, prop
type GenerateNextOptions = (optionText: string) => Promise<Option[]>
type Option = {
nameInMenu: string,
fullName: string,
abbreviatedName: string,
iconInMenu: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>, // type for lucide-react components
} & (
| { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined }
| { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined }
| { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
| { leafNodeType?: undefined, nextOptions: Option[], generateNextOptions?: undefined, }
| { leafNodeType?: undefined, nextOptions?: undefined, generateNextOptions: GenerateNextOptions, }
| { leafNodeType: 'File' | 'Folder', uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, }
)
@@ -131,7 +137,7 @@ const scoreSubsequence = (text: string, pattern: string): number => {
}
export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor>, uri: URI): string {
const workspaceService = accessor.get('IWorkspaceContextService');
const workspaceFolders = workspaceService.getWorkspace().folders;
@@ -159,7 +165,7 @@ export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor
if (relativePath.startsWith('/')) {
relativePath = relativePath.slice(1);
}
console.log({ folderPath, relativePath, uriPath });
// console.log({ folderPath, relativePath, uriPath });
return relativePath;
}
@@ -173,10 +179,19 @@ export function getRelativeWorkspacePath(accessor: ReturnType<typeof useAccessor
const numOptionsToShow = 100
// TODO make this unique based on other options
const getAbbreviatedName = (relativePath: string) => {
return getBasename(relativePath, 1)
}
const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path: string[], optionText: string): Promise<Option[]> => {
const toolsService = accessor.get('IToolsService')
const searchForFilesOrFolders = async (t: string, searchFor: 'files' | 'folders') => {
try {
@@ -193,8 +208,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
leafNodeType: 'File',
uri: uri,
iconInMenu: File,
nameInMenu: relativePath,
nameToPaste: getBasename(relativePath, 2),
fullName: relativePath,
abbreviatedName: getAbbreviatedName(relativePath),
}
})
return res
@@ -241,7 +256,6 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
for (let i = 0; i < pathParts.length - 1; i++) {
currentPath = i === 0 ? `/${pathParts[i]}` : `${currentPath}/${pathParts[i]}`;
console.log('filepath', currentPath);
// Create a proper directory URI
const directoryUri = URI.joinPath(
@@ -258,8 +272,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
leafNodeType: 'Folder',
uri: uri,
iconInMenu: Folder, // Folder
nameInMenu: relativePath,
nameToPaste: getBasename(relativePath, 2)
fullName: relativePath,
abbreviatedName: getAbbreviatedName(relativePath),
})) satisfies Option[];
}
} catch (error) {
@@ -271,13 +285,15 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
const allOptions: Option[] = [
{
nameInMenu: 'files',
fullName: 'files',
abbreviatedName: 'files',
iconInMenu: File,
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [],
},
{
nameInMenu: 'folders',
iconInMenu: FolderClosed,
fullName: 'folders',
abbreviatedName: 'folders',
iconInMenu: Folder,
generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [],
},
]
@@ -289,7 +305,7 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
for (const pn of path) {
const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase())
const selectedOption = nextOptionsAtPath.find(o => o.fullName.toLowerCase() === pn.toLowerCase())
if (!selectedOption) return [];
@@ -300,14 +316,20 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path:
if (generateNextOptionsAtPath) {
nextOptionsAtPath = await generateNextOptionsAtPath(optionText)
}
else if (path.length === 0 && optionText.trim().length > 0) { // (special case): directly search for both files and folders if optionsPath is empty and there's a search term
const filesResults = await searchForFilesOrFolders(optionText, 'files') || [];
const foldersResults = await searchForFilesOrFolders(optionText, 'folders') || [];
nextOptionsAtPath = [...foldersResults, ...filesResults,]
}
const optionsAtPath = nextOptionsAtPath
.filter(o => isSubsequence(o.nameInMenu, optionText))
.filter(o => isSubsequence(o.fullName, optionText))
.sort((a, b) => { // this is a hack but good for now
const scoreA = scoreSubsequence(a.nameInMenu, optionText);
const scoreB = scoreSubsequence(b.nameInMenu, optionText);
const scoreA = scoreSubsequence(a.fullName, optionText);
const scoreB = scoreSubsequence(b.fullName, optionText);
return scoreB - scoreA;
})
.slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints
@@ -354,6 +376,14 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
const [optionIdx, setOptionIdx] = useState<number>(0);
const [options, setOptions] = useState<Option[]>([]);
const [optionText, setOptionText] = useState<string>('');
const [didLoadInitialOptions, setDidLoadInitialOptions] = useState(false);
const currentPathRef = useRef<string>(JSON.stringify([]));
// dont show breadcrums if first page and user hasnt typed anything
const isTypingEnabled = true
const isBreadcrumbsShowing = optionPath.length === 0 && !optionText ? false : true
const insertTextAtCursor = (text: string) => {
const textarea = textAreaRef.current;
if (!textarea) return;
@@ -361,9 +391,21 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// Focus the textarea first
textarea.focus();
// The most reliable way to simulate typing is to use execCommand
// which will trigger all the appropriate native events
document.execCommand('insertText', false, text + ' '); // add space after too
// delete the @ and set the cursor position
// Get cursor position
const startPos = textarea.selectionStart;
const endPos = textarea.selectionEnd;
// Get the text before the cursor, excluding the @ symbol that triggered the menu
const textBeforeCursor = textarea.value.substring(0, startPos - 1);
const textAfterCursor = textarea.value.substring(endPos);
// Replace the text including the @ symbol with the selected option
textarea.value = textBeforeCursor + textAfterCursor;
// Set cursor position after the inserted text
const newCursorPos = textBeforeCursor.length;
textarea.setSelectionRange(newCursorPos, newCursorPos);
// React's onChange relies on a SyntheticEvent system
// The best way to ensure it runs is to call callbacks directly
@@ -379,51 +421,64 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
if (!options.length) { return; }
const option = options[optionIdx];
const newPath = [...optionPath, option.nameInMenu]
const newPath = [...optionPath, option.fullName]
const isLastOption = !option.generateNextOptions && !option.nextOptions
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
setDidLoadInitialOptions(false)
if (isLastOption) {
setIsMenuOpen(false)
insertTextAtCursor(option.nameToPaste)
insertTextAtCursor(option.abbreviatedName)
const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? {
let newSelection: StagingSelectionItem
if (option.leafNodeType === 'File') newSelection = {
type: 'File',
uri: option.uri,
language: languageService.guessLanguageIdByFilepathOrFirstLine(option.uri) || '',
state: { wasAddedAsCurrentFile: false }
} : option.leafNodeType === 'Folder' ? {
state: { wasAddedAsCurrentFile: false },
}
else if (option.leafNodeType === 'Folder') newSelection = {
type: 'Folder',
uri: option.uri,
language: undefined,
state: undefined,
} : (undefined as never)
}
else throw new Error(`Unexpected leafNodeType ${option.leafNodeType}`)
chatThreadService.addNewStagingSelection(newSelection)
console.log('selected', option.uri?.fsPath)
}
else {
currentPathRef.current = JSON.stringify(newPath);
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
setOptions(newOpts)
setDidLoadInitialOptions(true)
}
}
const onRemoveOption = async () => {
const newPath = [...optionPath.slice(0, optionPath.length - 1)]
currentPathRef.current = JSON.stringify(newPath);
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
if (currentPathRef.current !== JSON.stringify(newPath)) { return; }
setOptionPath(newPath)
setOptionText('')
setOptionIdx(0)
const newOpts = await getOptionsAtPath(accessor, newPath, '') || []
setOptions(newOpts)
}
const onOpenOptionMenu = async () => {
setOptionPath([])
const newPath: [] = []
currentPathRef.current = JSON.stringify([]);
const newOpts = await getOptionsAtPath(accessor, [], '') || []
if (currentPathRef.current !== JSON.stringify([])) { return; }
setOptionPath(newPath)
setOptionText('')
setIsMenuOpen(true);
setOptionIdx(0);
const newOpts = await getOptionsAtPath(accessor, [], '') || []
setOptions(newOpts);
}
const onCloseOptionMenu = () => {
@@ -466,24 +521,36 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
};
}, []);
// debounced
// debounced, but immediate if text is empty
const onPathTextChange = useCallback((newStr: string) => {
setOptionText(newStr);
if (debounceTimerRef.current !== null) {
window.clearTimeout(debounceTimerRef.current);
}
// Set a new timeout to fetch options after a delay
debounceTimerRef.current = window.setTimeout(async () => {
currentPathRef.current = JSON.stringify(optionPath);
const fetchOptions = async () => {
const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || [];
if (currentPathRef.current !== JSON.stringify(optionPath)) { return; }
setOptions(newOpts);
setOptionIdx(0);
debounceTimerRef.current = null;
}, 300);
};
// If text is empty, run immediately without debouncing
if (newStr.trim() === '') {
fetchOptions();
} else {
// Otherwise, set a new timeout to fetch options after a delay
debounceTimerRef.current = window.setTimeout(fetchOptions, 300);
}
}, [optionPath, accessor]);
const onMenuKeyDown = (e: React.KeyboardEvent<HTMLTextAreaElement>) => {
const isCommandKeyPressed = e.altKey || e.ctrlKey || e.metaKey;
@@ -537,7 +604,9 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// do nothing
}
else { // letter
onPathTextChange(optionText + e.key)
if (isTypingEnabled) {
onPathTextChange(optionText + e.key)
}
}
}
@@ -715,6 +784,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
return;
}
if (e.key === 'Backspace') { // TODO allow user to undo this.
if (!e.currentTarget.value || (e.currentTarget.selectionStart === 0 && e.currentTarget.selectionEnd === 0)) { // if there is no text or cursor is at position 0, remove a selection
if (e.metaKey || e.ctrlKey) { // Ctrl+Backspace = remove all
chatThreadService.popStagingSelections(Number.MAX_SAFE_INTEGER)
} else { // Backspace = pop 1 selection
chatThreadService.popStagingSelections(1)
}
return;
}
}
if (e.key === 'Enter') {
// Shift + Enter when multiline = newline
const shouldAddNewline = e.shiftKey && multiline
@@ -740,25 +819,25 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
onWheel={(e) => e.stopPropagation()}
>
{/* Breadcrumbs Header */}
<div className="px-2 py-1 text-void-fg-3 bg-void-bg-2-alt text-sm border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionPath.length || optionText ?
{isBreadcrumbsShowing && <div className="px-2 py-1 text-void-fg-1 bg-void-bg-2-alt border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionText ?
<div className="flex items-center">
{optionPath.map((path, index) => (
{/* {optionPath.map((path, index) => (
<React.Fragment key={index}>
<span>{path}</span>
<ChevronRight size={12} className="mx-1" />
</React.Fragment>
))}
))} */}
<span>{optionText}</span>
</div>
: <div className='opacity-60'>Enter text to filter...</div>
: <div className='opacity-50'>Enter text to filter...</div>
}
</div>
</div>}
{/* Options list */}
<div className='max-h-[400px] w-full max-w-full overflow-y-auto overflow-x-auto'>
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap text-sm opacity-70">
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap">
{options.length === 0 ?
<div className="text-void-fg-3 px-3 py-0.5">No results found</div>
: options.map((o, oIdx) => {
@@ -767,20 +846,25 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// Option
<div
ref={oIdx === optionIdx ? selectedOptionRef : null}
key={o.nameInMenu}
key={o.fullName}
className={`
flex items-center gap-2
px-3 py-0.5 cursor-pointer bg-void-bg-2-alt
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
px-3 py-1 cursor-pointer
${oIdx === optionIdx ? 'bg-blue-500 text-white/80' : 'bg-void-bg-2-alt text-void-fg-1'}
`}
onClick={() => { onSelectOption(); }}
onMouseOver={() => { setOptionIdx(oIdx) }}
onMouseMove={() => { setOptionIdx(oIdx) }}
>
{<o.iconInMenu size={12} />}
<span className="text-void-fg-1">{o.nameInMenu}</span>
<span>{o.abbreviatedName}</span>
{o.fullName && o.fullName !== o.abbreviatedName && <span className="opacity-60 text-sm">{o.fullName}</span>}
{o.nextOptions || o.generateNextOptions ? (
<ChevronRight size={12} />
) : null}
</div>
)
})
@@ -803,15 +887,44 @@ export const VoidSimpleInputBox = ({ value, onChangeValue, placeholder, classNam
compact?: boolean;
passwordBlur?: boolean;
} & React.InputHTMLAttributes<HTMLInputElement>) => {
// Create a ref for the input element to maintain the same DOM node between renders
const inputRef = useRef<HTMLInputElement>(null);
// Track if we need to restore selection
const selectionRef = useRef<{ start: number | null, end: number | null }>({
start: null,
end: null
});
// Handle value changes without recreating the input
useEffect(() => {
const input = inputRef.current;
if (input && input.value !== value) {
// Store current selection positions
selectionRef.current.start = input.selectionStart;
selectionRef.current.end = input.selectionEnd;
// Update the value
input.value = value;
// Restore selection if we had it before
if (selectionRef.current.start !== null && selectionRef.current.end !== null) {
input.setSelectionRange(selectionRef.current.start, selectionRef.current.end);
}
}
}, [value]);
const handleChange = useCallback((e: React.ChangeEvent<HTMLInputElement>) => {
onChangeValue(e.target.value);
}, [onChangeValue]);
return (
<input
value={value}
onChange={(e) => onChangeValue(e.target.value)}
ref={inputRef}
defaultValue={value} // Use defaultValue instead of value to avoid recreation
onChange={handleChange}
placeholder={placeholder}
disabled={disabled}
// className='max-w-44 w-full border border-void-border-2 bg-void-bg-1 text-void-fg-3 text-root'
// className={`w-full resize-none text-void-fg-1 placeholder:text-void-fg-3 px-2 py-1 rounded-sm
className={`w-full resize-none bg-void-bg-1 text-void-fg-1 placeholder:text-void-fg-3 border border-void-border-2 focus:border-void-border-1
${compact ? 'py-1 px-2' : 'py-2 px-4 '}
rounded
@@ -843,11 +956,11 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
const contextViewProvider = accessor.get('IContextViewService')
return <WidgetComponent
ctor={InputBox}
className='
bg-void-bg-1
@@void-force-child-placeholder-void-fg-1
'
ctor={InputBox}
propsFn={useCallback((container) => [
container,
contextViewProvider,
@@ -883,8 +996,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
inputBoxRef.current = instance;
return disposables
}, [onChangeText, onCreateInstance, inputBoxRef])
}
}, [onChangeText, onCreateInstance, inputBoxRef])}
/>
};
@@ -1329,7 +1441,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
key={optionName}
className={`flex items-center px-2 py-1 pr-4 cursor-pointer whitespace-nowrap
transition-all duration-100
${thisOptionIsSelected ? 'bg-void-bg-2-hover' : 'bg-void-bg-2-alt hover:bg-void-bg-2-hover'}
${thisOptionIsSelected ? 'bg-blue-500 text-white/80' : 'hover:bg-blue-500 hover:text-white/80'}
`}
onClick={() => {
onChangeOption(option);
@@ -1351,7 +1463,7 @@ export const VoidCustomDropdownBox = <T extends NonNullable<any>>({
</div>
<span className="flex justify-between items-center w-full gap-x-1">
<span>{optionName}</span>
<span className='text-void-fg-4 opacity-60'>{optionDetail}</span>
<span className='opacity-60'>{optionDetail}</span>
</span>
</div>
);
@@ -1733,3 +1845,154 @@ export const VoidButtonBgDarken = ({ children, disabled, onClick, className }: {
// };
const SingleDiffEditor = ({ block, lang }: { block: ExtractedSearchReplaceBlock, lang: string | undefined }) => {
const accessor = useAccessor();
const modelService = accessor.get('IModelService');
const instantiationService = accessor.get('IInstantiationService');
const languageService = accessor.get('ILanguageService');
const languageSelection = useMemo(() => languageService.createById(lang), [lang, languageService]);
// Create models for original and modified
const originalModel = useMemo(() =>
modelService.createModel(block.orig, languageSelection),
[block.orig, languageSelection, modelService]
);
const modifiedModel = useMemo(() =>
modelService.createModel(block.final, languageSelection),
[block.final, languageSelection, modelService]
);
// Clean up models on unmount
useEffect(() => {
return () => {
originalModel.dispose();
modifiedModel.dispose();
};
}, [originalModel, modifiedModel]);
// Imperatively mount the DiffEditorWidget
const divRef = useRef<HTMLDivElement | null>(null);
const editorRef = useRef<any>(null);
useEffect(() => {
if (!divRef.current) return;
// Create the diff editor instance
const editor = instantiationService.createInstance(
DiffEditorWidget,
divRef.current,
{
automaticLayout: true,
readOnly: true,
renderSideBySide: true,
minimap: { enabled: false },
lineNumbers: 'off',
scrollbar: {
vertical: 'hidden',
horizontal: 'auto',
verticalScrollbarSize: 0,
horizontalScrollbarSize: 8,
alwaysConsumeMouseWheel: false,
ignoreHorizontalScrollbarInContentHeight: true,
},
hover: { enabled: false },
folding: false,
selectionHighlight: false,
renderLineHighlight: 'none',
overviewRulerLanes: 0,
hideCursorInOverviewRuler: true,
overviewRulerBorder: false,
glyphMargin: false,
stickyScroll: { enabled: false },
scrollBeyondLastLine: false,
renderGutterMenu: false,
renderIndicators: false,
},
{ originalEditor: { isSimpleWidget: true }, modifiedEditor: { isSimpleWidget: true } }
);
editor.setModel({ original: originalModel, modified: modifiedModel });
// Calculate the height based on content
const updateHeight = () => {
const contentHeight = Math.max(
originalModel.getLineCount() * 19, // approximate line height
modifiedModel.getLineCount() * 19
) + 19 * 2 + 1; // add padding
// Set reasonable min/max heights
const height = Math.min(Math.max(contentHeight, 100), 300);
if (divRef.current) {
divRef.current.style.height = `${height}px`;
editor.layout();
}
};
updateHeight();
editorRef.current = editor;
// Update height when content changes
const disposable1 = originalModel.onDidChangeContent(() => updateHeight());
const disposable2 = modifiedModel.onDidChangeContent(() => updateHeight());
return () => {
disposable1.dispose();
disposable2.dispose();
editor.dispose();
editorRef.current = null;
};
}, [originalModel, modifiedModel, instantiationService]);
return (
<div className="w-full bg-void-bg-3 @@bg-editor-style-override" ref={divRef} />
);
};
/**
* ToolDiffEditor mounts a native VSCode DiffEditorWidget to show a diff between original and modified code blocks.
* Props:
* - uri: URI of the file (for language detection, etc)
* - searchReplaceBlocks: string in search/replace format (from LLM)
* - language?: string (optional, fallback to 'plaintext')
*/
export const VoidDiffEditor = ({ uri, searchReplaceBlocks, language }: { uri?: any, searchReplaceBlocks: string, language?: string }) => {
const accessor = useAccessor();
const languageService = accessor.get('ILanguageService');
// Extract all blocks
const blocks = extractSearchReplaceBlocks(searchReplaceBlocks);
// Use detectLanguage for language detection if not provided
let lang = language;
if (!lang && blocks.length > 0) {
lang = detectLanguage(languageService, { uri: uri ?? null, fileContents: blocks[0].orig });
}
// If no blocks, show empty state
if (blocks.length === 0) {
return <div className="w-full p-4 text-void-fg-4 text-sm">No changes found</div>;
}
// Display all blocks
return (
<div className="w-full flex flex-col gap-2">
{blocks.map((block, index) => (
<div key={index} className="w-full">
{blocks.length > 1 && (
<div className="text-void-fg-4 text-xs mb-1 px-1">
Change {index + 1} of {blocks.length}
</div>
)}
<SingleDiffEditor block={block} lang={lang} />
</div>
))}
</div>
);
};
@@ -4,9 +4,8 @@
*--------------------------------------------------------------------------------------*/
import React, { useState, useEffect, useCallback } from 'react'
import { RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSidebarState } from '../../../sidebarStateService.js'
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { DisposableStore, IDisposable } from '../../../../../../../base/common/lifecycle.js'
import { VoidSettingsState } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js'
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js'
import { RefreshModelStateOfProvider } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js'
@@ -22,8 +21,8 @@ import { IThemeService } from '../../../../../../../platform/theme/common/themeS
import { ILLMMessageService } from '../../../../common/sendLLMMessageService.js';
import { IRefreshModelService } from '../../../../../../../workbench/contrib/void/common/refreshModelService.js';
import { IVoidSettingsService } from '../../../../../../../workbench/contrib/void/common/voidSettingsService.js';
import { IExtensionTransferService } from '../../../../../../../workbench/contrib/void/browser/extensionTransferService.js'
import { ISidebarStateService } from '../../../sidebarStateService.js';
import { IInstantiationService } from '../../../../../../../platform/instantiation/common/instantiation.js'
import { ICodeEditorService } from '../../../../../../../editor/browser/services/codeEditorService.js'
import { ICommandService } from '../../../../../../../platform/commands/common/commands.js'
@@ -51,6 +50,10 @@ import { IToolsService } from '../../../toolsService.js'
import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService.js'
import { ITerminalService } from '../../../../../terminal/browser/terminal.js'
import { ISearchService } from '../../../../../../services/search/common/search.js'
import { IExtensionManagementService } from '../../../../../../../platform/extensionManagement/common/extensionManagement.js'
import { IMCPService } from '../../../../common/mcpService.js';
import { IStorageService, StorageScope } from '../../../../../../../platform/storage/common/storage.js'
import { OPT_OUT_KEY } from '../../../../common/storageKeys.js'
// normally to do this you'd use a useEffect that calls .onDidChangeState(), but useEffect mounts too late and misses initial state changes
@@ -58,9 +61,6 @@ import { ISearchService } from '../../../../../../services/search/common/search.
// even if React hasn't mounted yet, the variables are always updated to the latest state.
// React listens by adding a setState function to these listeners.
let sidebarState: VoidSidebarState
const sidebarStateListeners: Set<(s: VoidSidebarState) => void> = new Set()
let chatThreadsState: ThreadsState
const chatThreadsStateListeners: Set<(s: ThreadsState) => void> = new Set()
@@ -81,6 +81,8 @@ const ctrlKZoneStreamingStateListeners: Set<(diffareaid: number, s: boolean) =>
const commandBarURIStateListeners: Set<(uri: URI) => void> = new Set();
const activeURIListeners: Set<(uri: URI | null) => void> = new Set();
const mcpListeners: Set<() => 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!
@@ -91,7 +93,6 @@ export const _registerServices = (accessor: ServicesAccessor) => {
_registerAccessor(accessor)
const stateServices = {
sidebarStateService: accessor.get(ISidebarStateService),
chatThreadsStateService: accessor.get(IChatThreadService),
settingsStateService: accessor.get(IVoidSettingsService),
refreshModelService: accessor.get(IRefreshModelService),
@@ -99,17 +100,13 @@ export const _registerServices = (accessor: ServicesAccessor) => {
editCodeService: accessor.get(IEditCodeService),
voidCommandBarService: accessor.get(IVoidCommandBarService),
modelService: accessor.get(IModelService),
mcpService: accessor.get(IMCPService),
}
const { sidebarStateService, settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService, mcpService } = stateServices
sidebarState = sidebarStateService.state
disposables.push(
sidebarStateService.onDidChangeState(() => {
sidebarState = sidebarStateService.state
sidebarStateListeners.forEach(l => l(sidebarState))
})
)
chatThreadsState = chatThreadsStateService.state
disposables.push(
@@ -173,6 +170,11 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
disposables.push(
mcpService.onDidChangeState(() => {
mcpListeners.forEach(l => l())
})
)
return disposables
@@ -193,7 +195,6 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IRefreshModelService: accessor.get(IRefreshModelService),
IVoidSettingsService: accessor.get(IVoidSettingsService),
IEditCodeService: accessor.get(IEditCodeService),
ISidebarStateService: accessor.get(ISidebarStateService),
IChatThreadService: accessor.get(IChatThreadService),
IInstantiationService: accessor.get(IInstantiationService),
@@ -223,6 +224,11 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IToolsService: accessor.get(IToolsService),
IConvertToLLMMessageService: accessor.get(IConvertToLLMMessageService),
ITerminalService: accessor.get(ITerminalService),
IExtensionManagementService: accessor.get(IExtensionManagementService),
IExtensionTransferService: accessor.get(IExtensionTransferService),
IMCPService: accessor.get(IMCPService),
IStorageService: accessor.get(IStorageService),
} as const
return reactAccessor
@@ -250,16 +256,6 @@ export const useAccessor = () => {
// -- state of services --
export const useSidebarState = () => {
const [s, ss] = useState(sidebarState)
useEffect(() => {
ss(sidebarState)
sidebarStateListeners.add(ss)
return () => { sidebarStateListeners.delete(ss) }
}, [ss])
return s
}
export const useSettingsState = () => {
const [s, ss] = useState(settingsState)
useEffect(() => {
@@ -394,3 +390,39 @@ export const useActiveURI = () => {
export const useMCPServiceState = () => {
const accessor = useAccessor()
const mcpService = accessor.get('IMCPService')
const [s, ss] = useState(mcpService.state)
useEffect(() => {
const listener = () => { ss(mcpService.state) }
mcpListeners.add(listener);
return () => { mcpListeners.delete(listener) };
}, []);
return s
}
export const useIsOptedOut = () => {
const accessor = useAccessor()
const storageService = accessor.get('IStorageService')
const getVal = useCallback(() => {
return storageService.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
}, [storageService])
const [s, ss] = useState(getVal())
useEffect(() => {
const disposables = new DisposableStore();
const d = storageService.onDidChangeValue(StorageScope.APPLICATION, OPT_OUT_KEY, disposables)(e => {
ss(getVal())
})
disposables.add(d)
return () => disposables.clear()
}, [storageService, getVal])
return s
}
@@ -9,9 +9,19 @@ import { useAccessor, useCommandBarState, useIsDark } from '../util/services.js'
import '../styles.css'
import { useCallback, useEffect, useState, useRef } from 'react';
import { ScrollType } from '../../../../../../../editor/common/editorCommon.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { VoidCommandBarProps } from '../../../voidCommandBarService.js';
import { AcceptAllButtonWrapper, RejectAllButtonWrapper } from '../sidebar-tsx/SidebarChat.js';
import { Check, EllipsisVertical, Menu, MoveDown, MoveLeft, MoveRight, MoveUp, X } from 'lucide-react';
import {
VOID_GOTO_NEXT_DIFF_ACTION_ID,
VOID_GOTO_PREV_DIFF_ACTION_ID,
VOID_GOTO_NEXT_URI_ACTION_ID,
VOID_GOTO_PREV_URI_ACTION_ID,
VOID_ACCEPT_FILE_ACTION_ID,
VOID_REJECT_FILE_ACTION_ID,
VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
VOID_REJECT_ALL_DIFFS_ACTION_ID
} from '../../../actionIDs.js';
export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const isDark = useIsDark()
@@ -25,14 +35,55 @@ export const VoidCommandBarMain = ({ uri, editor }: VoidCommandBarProps) => {
const stepIdx = (currIdx: number | null, len: number, step: -1 | 1) => {
if (len === 0) return null
return ((currIdx ?? 0) + step + len) % len // for some reason, small negatives are kept negative. just add len to offset
}
export const AcceptAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={`
px-2 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
h-full rounded-none
cursor-pointer
${className}
`}
style={{
backgroundColor: 'var(--vscode-button-background)',
color: 'var(--vscode-button-foreground)',
border: 'none',
}}
type='button'
onClick={onClick}
{...props}
>
{text ? <span>{text}</span> : <Check size={16} />}
</button>
)
export const RejectAllButtonWrapper = ({ text, onClick, className, ...props }: { text: string, onClick: () => void, className?: string } & React.ButtonHTMLAttributes<HTMLButtonElement>) => (
<button
className={`
px-2 py-0.5
flex items-center gap-1
text-white text-[11px] text-nowrap
h-full rounded-none
cursor-pointer
${className}
`}
style={{
backgroundColor: 'var(--vscode-button-secondaryBackground)',
color: 'var(--vscode-button-secondaryForeground)',
border: 'none',
}}
type='button'
onClick={onClick}
{...props}
>
{text ? <span>{text}</span> : <X size={16} />}
</button>
)
const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
export const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const accessor = useAccessor()
const editCodeService = accessor.get('IEditCodeService')
const editorService = accessor.get('ICodeEditorService')
@@ -40,12 +91,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const commandService = accessor.get('ICommandService')
const commandBarService = accessor.get('IVoidCommandBarService')
const voidModelService = accessor.get('IVoidModelService')
const keybindingService = accessor.get('IKeybindingService')
const { stateOfURI: commandBarState, sortedURIs: sortedCommandBarURIs } = useCommandBarState()
// useEffect(() => {
// console.log('MOUNTING!!!')
// }, [])
const [showAcceptRejectAllButtons, setShowAcceptRejectAllButtons] = useState(false)
// latestUriIdx is used to remember place in leftRight
const _latestValidUriIdxRef = useRef<number | null>(null)
@@ -70,56 +118,18 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const s = commandBarService.stateOfURI[uri.fsPath]
if (!s) return
const { diffIdx } = s
goToDiffIdx(diffIdx ?? 0)
commandBarService.goToDiffIdx(diffIdx ?? 0)
}, 50)
}, [uri, commandBarService])
if (uri?.scheme !== 'file') return null // don't show in editors that we made, they must be files
const getNextDiffIdx = (step: 1 | -1) => {
// check undefined
if (!uri) return null
const s = commandBarState[uri.fsPath]
if (!s) return null
const { diffIdx, sortedDiffIds } = s
// get next idx
const nextDiffIdx = stepIdx(diffIdx, sortedDiffIds.length, step)
return nextDiffIdx
}
const goToDiffIdx = (idx: number | null) => {
if (idx === null) return
// check undefined
if (!uri) return
const s = commandBarState[uri.fsPath]
if (!s) return
const { sortedDiffIds } = s
// reveal
const diffid = sortedDiffIds[idx]
if (diffid === undefined) return
const diff = editCodeService.diffOfId[diffid]
if (!diff) return
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate)
commandBarService.setDiffIdx(uri, idx)
}
const getNextUriIdx = (step: 1 | -1) => {
return stepIdx(uriIdxInStepper, sortedCommandBarURIs.length, step)
}
const goToURIIdx = async (idx: number | null) => {
if (idx === null) return
const nextURI = sortedCommandBarURIs[idx]
editCodeService.diffAreasOfURI
const { model } = await voidModelService.getModelSafe(nextURI)
if (model) {
// switch to the URI
editorService.openCodeEditor({ resource: model.uri, options: { revealIfVisible: true } }, editor)
}
}
// Using service methods directly
const currDiffIdx = uri ? commandBarState[uri.fsPath]?.diffIdx ?? null : null
const sortedDiffIds = uri ? commandBarState[uri.fsPath]?.sortedDiffIds ?? [] : []
const sortedDiffZoneIds = uri ? commandBarState[uri.fsPath]?.sortedDiffZoneIds ?? [] : []
const isADiffInThisFile = sortedDiffIds.length !== 0
const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0
const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0
@@ -127,191 +137,247 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => {
const streamState = uri ? commandBarService.getStreamState(uri) : null
const showAcceptRejectAll = streamState === 'idle-has-changes'
const nextDiffIdx = getNextDiffIdx(1)
const prevDiffIdx = getNextDiffIdx(-1)
const nextURIIdx = getNextUriIdx(1)
const prevURIIdx = getNextUriIdx(-1)
const nextDiffIdx = commandBarService.getNextDiffIdx(1)
const prevDiffIdx = commandBarService.getNextDiffIdx(-1)
const nextURIIdx = commandBarService.getNextUriIdx(1)
const prevURIIdx = commandBarService.getNextUriIdx(-1)
const upDownDisabled = prevDiffIdx === null || nextDiffIdx === null
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null // || (sortedCommandBarURIs.length === 1 && isADiffZoneInThisFile)
const upButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}// --border border-void-border-3 focus:border-void-border-1
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(prevDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(prevDiffIdx);
}
}}
></button>
const downButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={upDownDisabled}
onClick={() => { goToDiffIdx(nextDiffIdx) }}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToDiffIdx(nextDiffIdx);
}
}}
></button>
const leftButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(prevURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(prevURIIdx);
}
}}
></button>
const rightButton = <button
className={`
size-6 rounded cursor-default
hover:bg-void-bg-1-alt
`}
disabled={leftRightDisabled}
onClick={() => goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
goToURIIdx(nextURIIdx);
}
}}
></button>
const leftRightDisabled = prevURIIdx === null || nextURIIdx === null
// accept/reject if current URI has changes
const onAcceptAll = () => {
const onAcceptFile = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'accept', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Accept All', {})
metricsService.capture('Accept File', {})
}
const onRejectAll = () => {
const onRejectFile = () => {
if (!uri) return
editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false, _addToHistory: true })
metricsService.capture('Reject All', {})
metricsService.capture('Reject File', {})
}
const onAcceptAll = () => {
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
metricsService.capture('Accept All', {})
setShowAcceptRejectAllButtons(false);
}
const onRejectAll = () => {
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
metricsService.capture('Reject All', {})
setShowAcceptRejectAllButtons(false);
}
const _upKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_DIFF_ACTION_ID);
const _downKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_DIFF_ACTION_ID);
const _leftKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_PREV_URI_ACTION_ID);
const _rightKeybinding = keybindingService.lookupKeybinding(VOID_GOTO_NEXT_URI_ACTION_ID);
const _acceptFileKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_FILE_ACTION_ID);
const _rejectFileKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_FILE_ACTION_ID);
const _acceptAllKeybinding = keybindingService.lookupKeybinding(VOID_ACCEPT_ALL_DIFFS_ACTION_ID);
const _rejectAllKeybinding = keybindingService.lookupKeybinding(VOID_REJECT_ALL_DIFFS_ACTION_ID);
const upKeybindLabel = editCodeService.processRawKeybindingText(_upKeybinding?.getLabel() || '');
const downKeybindLabel = editCodeService.processRawKeybindingText(_downKeybinding?.getLabel() || '');
const leftKeybindLabel = editCodeService.processRawKeybindingText(_leftKeybinding?.getLabel() || '');
const rightKeybindLabel = editCodeService.processRawKeybindingText(_rightKeybinding?.getLabel() || '');
const acceptFileKeybindLabel = editCodeService.processRawKeybindingText(_acceptFileKeybinding?.getAriaLabel() || '');
const rejectFileKeybindLabel = editCodeService.processRawKeybindingText(_rejectFileKeybinding?.getAriaLabel() || '');
const acceptAllKeybindLabel = editCodeService.processRawKeybindingText(_acceptAllKeybinding?.getAriaLabel() || '');
const rejectAllKeybindLabel = editCodeService.processRawKeybindingText(_rejectAllKeybinding?.getAriaLabel() || '');
if (!isADiffZoneInAnyFile) return null
// const acceptAllButton = <button
// className='text-nowrap'
// onClick={onAcceptAll}
// style={{
// backgroundColor: acceptAllBg,
// border: acceptBorder,
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Accept File
// </button>
// const rejectAllButton = <button
// className='text-nowrap'
// onClick={onRejectAll}
// style={{
// backgroundColor: rejectBg,
// border: rejectBorder,
// color: 'white',
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// >
// Reject File
// </button>
const acceptAllButton = <AcceptAllButtonWrapper
text={'Keep Changes'}
onClick={onAcceptAll}
/>
const rejectAllButton = <RejectAllButtonWrapper
text={'Reject All'}
onClick={onRejectAll}
/>
const acceptRejectAllButtons = <div className="flex items-center gap-1 text-sm">
{acceptAllButton}
{rejectAllButton}
</div>
// const closeCommandBar = useCallback(() => {
// commandService.executeCommand('void.hideCommandBar');
// }, [commandService]);
// const hideButton = <button
// className='ml-auto pointer-events-auto'
// onClick={closeCommandBar}
// style={{
// color: buttonTextColor,
// fontSize: buttonFontSize,
// padding: '2px 4px',
// borderRadius: '6px',
// cursor: 'pointer'
// }}
// title="Close command bar"
// >x
// </button>
const leftRightUpDownButtons = <div className='p-1 gap-1 flex flex-col items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 w-full'>
<div className="flex flex-col gap-1">
{/* Changes in file */}
<div className={`${!isADiffZoneInThisFile ? 'hidden' : ''} flex items-center ${upDownDisabled ? 'opacity-50' : ''}`}>
{upButton}
{downButton}
<span className="min-w-16 px-2 text-xs leading-[1]">
{isADiffInThisFile ?
`Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming' ?
'No changes yet'
: `No changes`
}
</span>
// For pages without a current file index, show a simplified command bar
if (currFileIdx === null) {
return (
<div className="pointer-events-auto">
<div className="flex bg-void-bg-2 shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
<div className="flex items-center px-3">
<span className="text-xs whitespace-nowrap">
{`${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`}
</span>
</div>
<button
className="text-xs whitespace-nowrap cursor-pointer flex items-center justify-center gap-1 bg-[var(--vscode-button-background)] text-[var(--vscode-button-foreground)] hover:opacity-90 h-full px-3"
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(nextURIIdx);
}
}}
>
Next <MoveRight className='size-3 my-1' />
</button>
</div>
</div>
);
}
{/* Files */}
<div className={`${!isADiffZoneInAnyFile ? 'hidden' : ''} flex items-center ${leftRightDisabled ? 'opacity-50' : ''}`}>
{leftButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
{rightButton}
{/* <div className="w-px h-3 bg-void-border-3 mx-0.5 shadow-sm"></div> */}
<span className="min-w-16 px-2 text-xs leading-[1]">
{currFileIdx !== null ?
`File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'} changed`
}
</span>
return (
<div className="pointer-events-auto">
{/* Accept All / Reject All buttons that appear when the vertical ellipsis is clicked */}
{showAcceptRejectAllButtons && showAcceptRejectAll && (
<div className="flex justify-end mb-1">
<div className="inline-flex bg-void-bg-2 rounded shadow-md border border-void-border-2 overflow-hidden">
<div className="flex items-center [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
<AcceptAllButtonWrapper
// text={`Accept All${acceptAllKeybindLabel ? ` ${acceptAllKeybindLabel}` : ''}`}
text={`Accept All`}
data-tooltip-id='void-tooltip'
data-tooltip-content={acceptAllKeybindLabel}
data-tooltip-delay-show={500}
onClick={onAcceptAll}
/>
<RejectAllButtonWrapper
// text={`Reject All${rejectAllKeybindLabel ? ` ${rejectAllKeybindLabel}` : ''}`}
text={`Reject All`}
data-tooltip-id='void-tooltip'
data-tooltip-content={rejectAllKeybindLabel}
data-tooltip-delay-show={500}
onClick={onRejectAll}
/>
</div>
</div>
</div>
)}
<div className="flex items-center bg-void-bg-2 rounded shadow-md border border-void-border-2 [&>*:first-child]:pl-3 [&>*:last-child]:pr-3 [&>*]:px-3 [&>*]:border-r [&>*]:border-void-border-2 [&>*:last-child]:border-r-0">
{/* Diff Navigation Group */}
<div className="flex items-center py-0.5">
<button
className="cursor-pointer"
disabled={upDownDisabled}
onClick={() => commandBarService.goToDiffIdx(prevDiffIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToDiffIdx(prevDiffIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${upKeybindLabel ? `${upKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveUp className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
<span className={`text-xs whitespace-nowrap px-1 ${!isADiffInThisFile ? 'opacity-70' : ''}`}>
{isADiffInThisFile
? `Diff ${(currDiffIdx ?? 0) + 1} of ${sortedDiffIds.length}`
: streamState === 'streaming'
? 'No changes yet'
: 'No changes'
}
</span>
<button
className="cursor-pointer"
disabled={upDownDisabled}
onClick={() => commandBarService.goToDiffIdx(nextDiffIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToDiffIdx(nextDiffIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${downKeybindLabel ? `${downKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveDown className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
</div>
{/* File Navigation Group */}
<div className="flex items-center py-0.5">
<button
className="cursor-pointer"
disabled={leftRightDisabled}
onClick={() => commandBarService.goToURIIdx(prevURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(prevURIIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${leftKeybindLabel ? `${leftKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveLeft className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
<span className="text-xs whitespace-nowrap px-1 mx-0.5">
{currFileIdx !== null
? `File ${currFileIdx + 1} of ${sortedCommandBarURIs.length}`
: `${sortedCommandBarURIs.length} file${sortedCommandBarURIs.length === 1 ? '' : 's'}`
}
</span>
<button
className="cursor-pointer"
disabled={leftRightDisabled}
onClick={() => commandBarService.goToURIIdx(nextURIIdx)}
onKeyDown={(e) => {
if (e.key === 'Enter' || e.key === ' ') {
e.preventDefault();
commandBarService.goToURIIdx(nextURIIdx);
}
}}
data-tooltip-id="void-tooltip"
data-tooltip-content={`${rightKeybindLabel ? `${rightKeybindLabel}` : ''}`}
data-tooltip-delay-show={500}
>
<MoveRight className='size-3 transition-opacity duration-200 opacity-70 hover:opacity-100' />
</button>
</div>
{/* Accept/Reject buttons - only shown when appropriate */}
{showAcceptRejectAll && (
<div className='flex self-stretch gap-0 !px-0 !py-0'>
<AcceptAllButtonWrapper
// text={`Accept File${acceptFileKeybindLabel ? ` ${acceptFileKeybindLabel}` : ''}`}
text={`Accept File`}
data-tooltip-id='void-tooltip'
data-tooltip-content={acceptFileKeybindLabel}
data-tooltip-delay-show={500}
onClick={onAcceptFile}
/>
<RejectAllButtonWrapper
// text={`Reject File${rejectFileKeybindLabel ? ` ${rejectFileKeybindLabel}` : ''}`}
text={`Reject File`}
data-tooltip-id='void-tooltip'
data-tooltip-content={rejectFileKeybindLabel}
data-tooltip-delay-show={500}
onClick={onRejectFile}
/>
</div>
)}
{/* Triple colon menu button */}
{showAcceptRejectAll && <div className='!px-0 !py-0 self-stretch flex justify-center items-center'>
<div
className="cursor-pointer px-1 self-stretch flex justify-center items-center"
onClick={() => setShowAcceptRejectAllButtons(!showAcceptRejectAllButtons)}
>
<EllipsisVertical
className="size-3"
/>
</div>
</div>}
</div>
</div>
</div>
return <div className={`flex flex-col items-center gap-y-2 pointer-events-auto`}>
{showAcceptRejectAll && acceptRejectAllButtons}
{leftRightUpDownButtons}
</div>
)
}
@@ -6,10 +6,9 @@
import { useEffect, useRef, useState } from 'react';
import { useAccessor, useIsDark, useSettingsState } from '../util/services.js';
import { Brain, Check, ChevronRight, DollarSign, ExternalLink, Lock, X } from 'lucide-react';
import { displayInfoOfProviderName, ProviderName, providerNames, refreshableProviderNames } from '../../../../common/voidSettingsTypes.js';
import { getModelCapabilities, ollamaRecommendedModels } from '../../../../common/modelCapabilities.js';
import { displayInfoOfProviderName, ProviderName, providerNames, localProviderNames, featureNames, FeatureName, isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js';
import { ChatMarkdownRender } from '../markdown/ChatMarkdownRender.js';
import { AddModelInputBox, AnimatedCheckmarkButton, OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider } from '../void-settings-tsx/Settings.js';
import { OllamaSetupInstructions, OneClickSwitchButton, SettingsForProvider, ModelDump } from '../void-settings-tsx/Settings.js';
import { ColorScheme } from '../../../../../../../platform/theme/common/theme.js';
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js';
import { isLinux } from '../../../../../../../base/common/platform.js';
@@ -27,9 +26,10 @@ export const VoidOnboarding = () => {
<div className={`@@void-scope ${isDark ? 'dark' : ''}`}>
<div
className={`
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full h-full z-[99999]
bg-void-bg-3 fixed top-0 right-0 bottom-0 left-0 width-full z-[99999]
transition-all duration-1000 ${isOnboardingComplete ? 'opacity-0 pointer-events-none' : 'opacity-100 pointer-events-auto'}
`}
style={{ height: '100vh', display: 'flex', alignItems: 'center', justifyContent: 'center' }}
>
<ErrorBoundary>
<VoidOnboardingContent />
@@ -90,6 +90,188 @@ const FadeIn = ({ children, className, delayMs = 0, durationMs, ...props }: { ch
}
// Onboarding
// =============================================
// New AddProvidersPage Component and helpers
// =============================================
const tabNames = ['Free', 'Paid', 'Local'] as const;
type TabName = typeof tabNames[number] | 'Cloud/Other';
// Data for cloud providers tab
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'awsBedrock', 'openAICompatible'];
// Data structures for provider tabs
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
Free: ['gemini', 'openRouter'],
Local: localProviderNames,
Paid: providerNames.filter(pn => !(['gemini', 'openRouter', ...localProviderNames, ...cloudProviders] as string[]).includes(pn)) as ProviderName[],
'Cloud/Other': cloudProviders,
};
const descriptionOfTab: Record<TabName, string> = {
Free: `Providers with a 100% free tier. Add as many as you'd like!`,
Paid: `Connect directly with any provider (bring your own key).`,
Local: `Active providers should appear automatically. Add as many as you'd like! `,
'Cloud/Other': `Add as many as you'd like! Reach out for custom configuration requests.`,
};
const featureNameMap: { display: string, featureName: FeatureName }[] = [
{ display: 'Chat', featureName: 'Chat' },
{ display: 'Quick Edit', featureName: 'Ctrl+K' },
{ display: 'Autocomplete', featureName: 'Autocomplete' },
{ display: 'Fast Apply', featureName: 'Apply' },
{ display: 'Source Control', featureName: 'SCM' },
];
const AddProvidersPage = ({ pageIndex, setPageIndex }: { pageIndex: number, setPageIndex: (index: number) => void }) => {
const [currentTab, setCurrentTab] = useState<TabName>('Free');
const settingsState = useSettingsState();
const [errorMessage, setErrorMessage] = useState<string | null>(null);
// Clear error message after 5 seconds
useEffect(() => {
let timeoutId: NodeJS.Timeout | null = null;
if (errorMessage) {
timeoutId = setTimeout(() => {
setErrorMessage(null);
}, 5000);
}
// Cleanup function to clear the timeout if component unmounts or error changes
return () => {
if (timeoutId) {
clearTimeout(timeoutId);
}
};
}, [errorMessage]);
return (<div className="flex flex-col md:flex-row w-full h-[80vh] gap-6 max-w-[900px] mx-auto relative">
{/* Left Column */}
<div className="md:w-1/4 w-full flex flex-col gap-6 p-6 border-none border-void-border-2 h-full overflow-y-auto">
{/* Tab Selector */}
<div className="flex md:flex-col gap-2">
{[...tabNames, 'Cloud/Other'].map(tab => (
<button
key={tab}
className={`py-2 px-4 rounded-md text-left ${currentTab === tab
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'
} transition-all duration-200`}
onClick={() => {
setCurrentTab(tab as TabName);
setErrorMessage(null); // Reset error message when changing tabs
}}
>
{tab}
</button>
))}
</div>
{/* Feature Checklist */}
<div className="flex flex-col gap-1 mt-4 text-sm opacity-80">
{featureNameMap.map(({ display, featureName }) => {
const hasModel = settingsState.modelSelectionOfFeature[featureName] !== null;
return (
<div key={featureName} className="flex items-center gap-2">
{hasModel ? (
<Check className="w-4 h-4 text-emerald-500" />
) : (
<div className="w-3 h-3 rounded-full flex items-center justify-center">
<div className="w-1 h-1 rounded-full bg-white/70"></div>
</div>
)}
<span>{display}</span>
</div>
);
})}
</div>
</div>
{/* Right Column */}
<div className="flex-1 flex flex-col items-center justify-start p-6 h-full overflow-y-auto">
<div className="text-5xl mb-2 text-center w-full">Add a Provider</div>
<div className="w-full max-w-xl mt-4 mb-10">
<div className="text-4xl font-light my-4 w-full">{currentTab}</div>
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">{descriptionOfTab[currentTab]}</div>
</div>
{providerNamesOfTab[currentTab].map((providerName) => (
<div key={providerName} className="w-full max-w-xl mb-10">
<div className="text-xl mb-2">
Add {displayInfoOfProviderName(providerName).title}
{providerName === 'gemini' && (
<span
data-tooltip-id="void-tooltip-provider-info"
data-tooltip-content="Gemini 2.5 Pro offers 25 free messages a day, and Gemini 2.5 Flash offers 500. We recommend using models down the line as you run out of free credits."
data-tooltip-place="right"
className="ml-1 text-xs align-top text-blue-400"
>*</span>
)}
{providerName === 'openRouter' && (
<span
data-tooltip-id="void-tooltip-provider-info"
data-tooltip-content="OpenRouter offers 50 free messages a day, and 1000 if you deposit $10. Only applies to models labeled ':free'."
data-tooltip-place="right"
className="ml-1 text-xs align-top text-blue-400"
>*</span>
)}
</div>
<div>
<SettingsForProvider providerName={providerName} showProviderTitle={false} showProviderSuggestions={true} />
</div>
{providerName === 'ollama' && <OllamaSetupInstructions />}
</div>
))}
{(currentTab === 'Local' || currentTab === 'Cloud/Other') && (
<div className="w-full max-w-xl mt-8 bg-void-bg-2/50 rounded-lg p-6 border border-void-border-4">
<div className="flex items-center gap-2 mb-4">
<div className="text-xl font-medium">Models</div>
</div>
{currentTab === 'Local' && (
<div className="text-sm opacity-80 text-void-fg-3 my-4 w-full">Local models should be detected automatically. You can add custom models below.</div>
)}
{currentTab === 'Local' && <ModelDump filteredProviders={localProviderNames} />}
{currentTab === 'Cloud/Other' && <ModelDump filteredProviders={cloudProviders} />}
</div>
)}
{/* Navigation buttons in right column */}
<div className="flex flex-col items-end w-full mt-auto pt-8">
{errorMessage && (
<div className="text-amber-400 mb-2 text-sm opacity-80 transition-opacity duration-300">{errorMessage}</div>
)}
<div className="flex items-center gap-2">
<PreviousButton onClick={() => setPageIndex(pageIndex - 1)} />
<NextButton
onClick={() => {
const isDisabled = isFeatureNameDisabled('Chat', settingsState)
if (!isDisabled) {
setPageIndex(pageIndex + 1);
setErrorMessage(null);
} else {
// Show error message
setErrorMessage("Please set up at least one Chat model before moving on.");
}
}}
/>
</div>
</div>
</div>
</div>);
};
// =============================================
// OnboardingPage
// title:
// div
@@ -179,7 +361,7 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
className?: string,
}) => {
return (
<div className={`min-h-full text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
<div className={`h-[80vh] text-lg flex flex-col gap-4 w-full mx-auto ${hasMaxWidth ? 'max-w-[600px]' : ''} ${className}`}>
{top && <FadeIn className='w-full mb-auto pt-16'>{top}</FadeIn>}
{content && <FadeIn className='w-full my-auto'>{content}</FadeIn>}
{bottom && <div className='w-full pb-8'>{bottom}</div>}
@@ -188,8 +370,6 @@ const OnboardingPageShell = ({ top, bottom, content, hasMaxWidth = true, classNa
}
const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb }: { modelName: string, isModelInstalled: boolean, sizeGb: number | false | 'not-known' }) => {
// for now just link to the ollama download page
return <a
href={`https://ollama.com/library/${modelName}`}
@@ -200,76 +380,6 @@ const OllamaDownloadOrRemoveModelButton = ({ modelName, isModelInstalled, sizeGb
<ExternalLink className="w-3.5 h-3.5" />
</a>
// if (isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Uninstall</span>
// <IconShell1
// className="ml-1"
// Icon={Trash}
// onClick={() => {
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (isModelInstalling) {
// return <div className="flex items-center">
// <span className="flex items-center">{`Download? ${typeof sizeGb === 'number' ? `(${sizeGb} Gb)` : ''}`}</span>
// <IconShell1
// className="ml-1"
// Icon={Square}
// onClick={() => {
// // abort()
// // TODO!!!!!!!!!!! don't do this
// setIsModelInstalling(false);
// }}
// />
// </div>
// }
// else if (!isModelInstalled) {
// return <div className="flex items-center">
// <span className="flex items-center">Download ({sizeGb} Gb)</span>
// <IconShell1
// className="ml-1"
// Icon={Download}
// onClick={() => {
// // this is a check for whether the model was installed:
// if (isModelInstalling) return
// // TODO!!!!!! don't do this
// // install(modelname), callback = setIsModelInstalling(false);
// setIsModelInstalling(true);
// }}
// />
// </div>
// }
// return <></>
}
@@ -306,114 +416,7 @@ const abbreviateNumber = (num: number): string => {
}
}
const TableOfModelsForProvider = ({ providerName }: { providerName: ProviderName }) => {
const accessor = useAccessor()
const voidSettingsService = accessor.get('IVoidSettingsService')
const voidSettingsState = useSettingsState()
const isDetectableLocally = (refreshableProviderNames as ProviderName[]).includes(providerName)
// const providerCapabilities = getProviderCapabilities(providerName)
// info used to show the table
const infoOfModelName: Record<string, { showAsDefault: boolean, isDownloaded: boolean } | undefined> = {}
voidSettingsState.settingsOfProvider[providerName].models.forEach(m => {
infoOfModelName[m.modelName] = {
showAsDefault: m.type !== 'custom',
isDownloaded: true
}
})
// special case columns for ollama; show recommended models as default
if (providerName === 'ollama') {
for (const modelName of ollamaRecommendedModels) {
if (modelName in infoOfModelName) continue
infoOfModelName[modelName] = {
isDownloaded: infoOfModelName[modelName]?.isDownloaded ?? false,
showAsDefault: true,
}
}
}
return <table className="table-fixed border-collapse mb-6 bg-void-bg-2 text-sm mx-auto select-text">
<thead>
<tr className="border-b border-void-border-1 text-nowrap text-ellipsis">
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[200px]">Models Offered</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Cost/M</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Context</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Chat</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Agent</th>
<th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Autotab</th>
{/* <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Reasoning</th> */}
{isDetectableLocally && <th className="text-left py-2 px-3 font-normal text-void-fg-3 min-w-[10%]">Detected</th>}
{providerName === 'ollama' && <th className="text-left py-2 px-3 font-normal text-void-fg-3">Download</th>}
</tr>
</thead>
<tbody>
{Object.keys(infoOfModelName).map(modelName => {
const { showAsDefault, isDownloaded } = infoOfModelName[modelName] ?? {}
const capabilities = getModelCapabilities(providerName, modelName)
const {
downloadable,
cost,
supportsFIM,
reasoningCapabilities,
contextWindow,
isUnrecognizedModel,
maxOutputTokens,
supportsSystemMessage,
} = capabilities
// TODO update this when tools work
const removeModelButton = <button
className="absolute -left-1 top-1/2 transform -translate-y-1/2 -translate-x-full text-void-fg-3 hover:text-void-fg-1 text-xs"
onClick={() => voidSettingsService.deleteModel(providerName, modelName)}
>
<X className="w-3.5 h-3.5" />
</button>
return (
<tr key={`${modelName}${providerName}`} className="border-b border-void-border-1 hover:bg-void-bg-3/50">
<td className="py-2 px-3 relative">
{!showAsDefault && removeModelButton}
{modelName}
</td>
<td className="py-2 px-3">${cost.output ?? ''}</td>
<td className="py-2 px-3">{contextWindow ? abbreviateNumber(contextWindow) : ''}</td>
<td className="py-2 px-3"><YesNoText val={true} /></td>
<td className="py-2 px-3"><YesNoText val={!!true} /></td>
<td className="py-2 px-3"><YesNoText val={!!supportsFIM} /></td>
{/* <td className="py-2 px-3"><YesNoText val={!!reasoningCapabilities} /></td> */}
{isDetectableLocally && <td className="py-2 px-3 flex items-center justify-center">{!!isDownloaded ? <Check className="w-4 h-4" /> : <></>}</td>}
{providerName === 'ollama' && <th className="py-2 px-3">
<OllamaDownloadOrRemoveModelButton modelName={modelName} isModelInstalled={!!infoOfModelName[modelName]?.isDownloaded} sizeGb={downloadable && downloadable.sizeGb} />
</th>}
</tr>
)
})}
<tr className="hover:bg-void-bg-3/50">
<td className="py-2 px-3 text-void-accent">
<ErrorBoundary>
<AddModelInputBox
key={providerName}
providerName={providerName}
compact={true}
/>
</ErrorBoundary>
</td>
<td colSpan={4}></td>
</tr>
</tbody>
</table>
}
@@ -431,22 +434,16 @@ const PrimaryActionButton = ({ children, className, ringSize, ...props }: { chil
${ringSize === 'xl' ? `
gap-2 px-16 py-8
hover:ring-8 active:ring-8
transition-all duration-300 ease-in-out
`
: ringSize === 'screen' ? `
gap-2 px-16 py-8
ring-[3000px]
transition-all duration-1000 ease-in-out
`: ringSize === undefined ? `
gap-1 px-4 py-2
hover:ring-2 active:ring-2
transition-all duration-300 ease-in-out
`: ''}
hover:ring-black/90 dark:hover:ring-white/90
active:ring-black/90 dark:active:ring-white/90
rounded-lg
group
${className}
@@ -534,7 +531,6 @@ const VoidOnboardingContent = () => {
/>
<NextButton
onClick={() => { setPageIndex(pageIndex + 1) }}
disabled={pageIndex === 2 && !didFillInSelectedProviderSettings}
/>
</div>
</div>
@@ -612,7 +608,7 @@ const VoidOnboardingContent = () => {
delayMs={1000}
>
<PrimaryActionButton
onClick={() => { setPageIndex(pageIndex + 1) }}
onClick={() => { setPageIndex(1) }}
>
Get Started
</PrimaryActionButton>
@@ -621,255 +617,13 @@ const VoidOnboardingContent = () => {
</div>
}
/>,
1: <OnboardingPageShell
hasMaxWidth={false}
top={<></>}
content={<div className='flex flex-col items-center -translate-y-[20vh]'>
{/* <div className="text-5xl text-center mb-8">AI Preferences</div> */}
<div className="text-4xl text-void-fg-2 mb-8 text-center">Model Preferences</div>
<div className="grid grid-cols-1 md:grid-cols-3 gap-6 w-full max-w-[800px] mx-auto mt-8">
<button
onClick={() => { setWantToUseOption('cheap'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<DollarSign size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Affordable</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['cheap']}</div>
</button>
<button
onClick={() => { setWantToUseOption('private'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Lock size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Private</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['private']}</div>
</button>
<button
onClick={() => { setWantToUseOption('smart'); setPageIndex(pageIndex + 1); }}
className="flex flex-col p-6 rounded bg-void-bg-2 border border-void-border-3 hover:brightness-110 transition-colors focus:outline-none focus:border-void-accent-border relative overflow-hidden min-h-[160px]"
>
<div className="flex items-center mb-3">
<Brain size={24} className="text-void-fg-2 mr-2" />
<div className="text-lg font-medium text-void-fg-1">Intelligent</div>
</div>
<div className="text-sm text-void-fg-2 text-left">{basicDescOfWantToUseOption['smart']}</div>
</button>
</div>
</div>}
bottom={
<div className='mx-auto w-full max-w-[800px]'>
<PreviousButton onClick={() => { setPageIndex(pageIndex - 1) }} />
</div>
1: <OnboardingPageShell hasMaxWidth={false}
content={
<AddProvidersPage pageIndex={pageIndex} setPageIndex={setPageIndex} />
}
/>,
2: <OnboardingPageShell
top={
<>
{/* Title */}
<div className="text-5xl font-light text-center mt-[10vh] mb-6">Choose a Provider</div>
{/* Preference Selector */}
<div
className="mb-6 w-fit mx-auto flex items-center overflow-hidden bg-zinc-700/5 dark:bg-zinc-300/5 rounded-md"
>
{[
{ id: 'smart', label: 'Intelligent' },
{ id: 'private', label: 'Private' },
{ id: 'cheap', label: 'Affordable' },
{ id: 'all', label: 'All' }
].map(option => (
<ErrorBoundary
key={option.id}
>
<button
onClick={() => setWantToUseOption(option.id as WantToUseOption)}
className={`py-1 px-2 text-xs cursor-pointer whitespace-nowrap rounded-sm transition-colors ${wantToUseOption === option.id
? 'dark:text-white text-black font-medium'
: 'text-void-fg-3 hover:text-void-fg-2'
}`}
data-tooltip-id='void-tooltip'
data-tooltip-content={`${option.label} providers`}
data-tooltip-place='bottom'
>
{option.label}
</button>
</ErrorBoundary>
))}
</div>
{/* Provider Buttons - Modified to use separate components for each tab */}
<div className="mb-2 w-full">
{/* Intelligent tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'smart' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['smart'].map((providerName) => {
const isSelected = selectedIntelligentProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedIntelligentProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Private tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'private' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['private'].map((providerName) => {
const isSelected = selectedPrivateProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedPrivateProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* Affordable tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'cheap' ? 'flex' : 'hidden'}`}>
{providerNamesOfWantToUseOption['cheap'].map((providerName) => {
const isSelected = selectedAffordableProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAffordableProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
{/* All tab */}
<ErrorBoundary>
<div className={`flex flex-wrap items-center w-full ${wantToUseOption === 'all' ? 'flex' : 'hidden'}`}>
{providerNames.map((providerName) => {
const isSelected = selectedAllProvider === providerName;
return (
<button
key={providerName}
onClick={() => setSelectedAllProvider(providerName)}
className={`py-[2px] px-2 mx-0.5 my-0.5 text-xs font-medium cursor-pointer relative rounded-full transition-all duration-300
${isSelected ? 'bg-zinc-100 text-zinc-900 shadow-sm border-white/80' : 'bg-zinc-100/40 hover:bg-zinc-100/50 text-zinc-900 border-white/20'}`}
>
{displayInfoOfProviderName(providerName).title}
</button>
);
})}
</div>
</ErrorBoundary>
</div>
{/* Description */}
<ErrorBoundary>
<div className="text-left self-start text-sm text-void-fg-3 px-2 py-1">
<ChatMarkdownRender string={detailedDescOfWantToUseOption[wantToUseOption]} chatMessageLocation={undefined} />
</div>
</ErrorBoundary>
{/* ModelsTable and ProviderFields */}
{selectedProviderName && <div className='mt-4 w-fit mx-auto'>
{/* Models Table */}
<ErrorBoundary>
<TableOfModelsForProvider providerName={selectedProviderName} />
</ErrorBoundary>
{/* Add provider section - simplified styling */}
<div className='mb-5 mt-8 mx-auto'>
<ErrorBoundary>
<div className=''>
Add {displayInfoOfProviderName(selectedProviderName).title}
<div className='my-4'>
{selectedProviderName === 'ollama' ? <OllamaSetupInstructions /> : ''}
</div>
</div>
</ErrorBoundary>
<ErrorBoundary>
{selectedProviderName &&
<SettingsForProvider providerName={selectedProviderName} showProviderTitle={false} showProviderSuggestions={false} />
}
</ErrorBoundary>
{/* Button and status indicators */}
<ErrorBoundary>
{!didFillInProviderSettings ? <p className="text-xs text-void-fg-3 mt-2">Please fill in all fields to continue</p>
: !isAtLeastOneModel ? <p className="text-xs text-void-fg-3 mt-2">Please add a model to continue</p>
: !isApiKeyLongEnoughIfApiKeyExists ? <p className="text-xs text-void-fg-3 mt-2">Please enter a valid API key</p>
: <AnimatedCheckmarkButton className='text-xs text-void-fg-3 mt-2' text='Added' />}
</ErrorBoundary>
</div>
</div>}
</>
}
bottom={
<ErrorBoundary>
<FadeIn delayMs={50} durationMs={10}>
{prevAndNextButtons}
</FadeIn>
</ErrorBoundary>
}
/>,
// 2.5: <div className="max-w-[600px] w-full h-full text-left mx-auto flex flex-col items-center justify-between">
// <FadeIn>
// <div className="text-5xl font-light mb-6 mt-12 text-center">Autocomplete</div>
// <div className="text-center flex flex-col gap-4 w-full max-w-md mx-auto">
// <h4 className="text-void-fg-3 mb-2">Void offers free autocomplete with locally hosted models</h4>
// <h4 className="text-void-fg-3 mb-2">[have buttons for Ollama install Qwen2.5coder3b and memory requirements] </h4>
// </div>
// </FadeIn>
// {prevAndNextButtons}
// </div>,
3: <OnboardingPageShell
content={
<div>
@@ -884,32 +638,11 @@ const VoidOnboardingContent = () => {
</div>
}
bottom={lastPagePrevAndNextButtons}
// bottom={prevAndNextButtons}
/>,
// 4: <OnboardingPageShell
// content={
// <>
// <div
// className='flex justify-center'
// >
// <PrimaryActionButton
// onClick={() => { voidSettingsService.setGlobalSetting('isOnboardingComplete', true); }}
// ringSize={voidSettingsState.globalSettings.isOnboardingComplete ? 'screen' : undefined}
// className='text-4xl'
// >Enter the Void</PrimaryActionButton>
// </div>
// </>
// }
// bottom={
// <PreviousButton
// onClick={() => { setPageIndex(pageIndex - 1) }}
// />
// }
// />,
}
return <div key={pageIndex} className="w-full h-full text-left mx-auto overflow-y-scroll flex flex-col items-center justify-around">
return <div key={pageIndex} className="w-full h-[80vh] text-left mx-auto flex flex-col items-center justify-center">
<ErrorBoundary>
{contentOfIdx[pageIndex]}
</ErrorBoundary>
@@ -56,7 +56,7 @@ const MemoizedModelDropdown = ({ featureName, className }: { featureName: Featur
useEffect(() => {
const oldOptions = oldOptionsRef.current
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode }))
const newOptions = settingsState._modelOptions.filter((o) => filter(o.selection, { chatMode: settingsState.globalSettings.chatMode, overridesOfModel: settingsState.overridesOfModel }))
if (!optionsEqual(oldOptions, newOptions)) {
setMemoizedOptions(newOptions)
File diff suppressed because it is too large Load Diff
@@ -45,11 +45,13 @@ export const VoidTooltip = () => {
<>
<style>
{`
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings {
#void-tooltip, #void-tooltip-orange, #void-tooltip-green, #void-tooltip-ollama-settings, #void-tooltip-provider-info {
font-size: 12px;
padding: 0px 8px;
border-radius: 6px;
z-index: 999999;
max-width: 300px;
word-wrap: break-word;
}
#void-tooltip {
@@ -67,7 +69,7 @@ export const VoidTooltip = () => {
color: white;
}
#void-tooltip-ollama-settings {
#void-tooltip-ollama-settings, #void-tooltip-provider-info {
background-color: var(--vscode-editor-background);
color: var(--vscode-input-foreground);
}
@@ -112,14 +114,25 @@ export const VoidTooltip = () => {
</div>
<div style={{ marginBottom: 4 }}>
<span style={{ opacity: 0.8 }}>For chat:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>llama3.1</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>gemma3</span>
</div>
<div>
<div style={{ marginBottom: 4 }}>
<span style={{ opacity: 0.8 }}>For autocomplete:{` `}</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder:1.5b</span>
<span style={{ opacity: 0.8, fontWeight: 'bold' }}>qwen2.5-coder</span>
</div>
<div style={{ marginBottom: 0 }}>
<span style={{ opacity: 0.8 }}>Use the largest version of these you can!</span>
</div>
</div>
</Tooltip>
<Tooltip
id="void-tooltip-provider-info"
border='1px solid rgba(100,100,100,.2)'
opacity={1}
delayShow={50}
style={{ pointerEvents: 'all', userSelect: 'text', fontSize: 11, maxWidth: '280px', paddingTop:'8px', paddingBottom:'8px' }}
/>
</>
);
};
@@ -14,23 +14,18 @@ import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextke
import { ICodeEditorService } from '../../../../editor/browser/services/codeEditorService.js';
import { IRange } from '../../../../editor/common/core/range.js';
import { VOID_VIEW_ID } from './sidebarPane.js';
import { VOID_VIEW_CONTAINER_ID, VOID_VIEW_ID } from './sidebarPane.js';
import { IMetricsService } from '../common/metricsService.js';
import { ISidebarStateService } from './sidebarStateService.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { VOID_TOGGLE_SETTINGS_ACTION_ID } from './voidSettingsPane.js';
import { VOID_CTRL_L_ACTION_ID } from './actionIDs.js';
import { ICodeEditor } from '../../../../editor/browser/editorBrowser.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { localize2 } from '../../../../nls.js';
import { StagingSelectionItem } from '../common/chatThreadServiceTypes.js';
import { IChatThreadService } from './chatThreadService.js';
import { getActiveWindow } from '../../../../base/browser/dom.js';
import { IViewsService } from '../../../services/views/common/viewsService.js';
// ---------- Register commands and keybindings ----------
export const roundRangeToLines = (range: IRange | null | undefined, options: { emptySelectionBehavior: 'null' | 'line' }) => {
if (!range)
return null
@@ -72,68 +67,21 @@ registerAction2(class extends Action2 {
super({ id: VOID_OPEN_SIDEBAR_ACTION_ID, title: localize2('voidOpenSidebar', 'Void: Open Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
stateService.fireFocusChat()
const viewsService = accessor.get(IViewsService)
const chatThreadsService = accessor.get(IChatThreadService)
viewsService.openViewContainer(VOID_VIEW_CONTAINER_ID)
await chatThreadsService.focusCurrentChat()
}
})
// Action: when press ctrl+L, show the sidebar chat and add to the selection
const VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID = 'void.sidebar.select'
registerAction2(class extends Action2 {
constructor() {
super({ id: VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID, title: localize2('voidAddToSidebar', 'Void: Add Selection to Sidebar'), f1: true });
}
async run(accessor: ServicesAccessor): Promise<void> {
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
if (!model)
return
const metricsService = accessor.get(IMetricsService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('Ctrl+L', {})
const editor = editorService.getActiveCodeEditor()
// accessor.get(IEditorService).activeTextEditorControl?.getSelection()
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// select whole lines
if (selectionRange) {
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
}
const newSelection: StagingSelectionItem = !selectionRange || (selectionRange.startLineNumber > selectionRange.endLineNumber) ? {
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
state: { wasAddedAsCurrentFile: false }
} : {
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false }
}
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.addNewStagingSelection(newSelection)
}
});
// cmd L
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_CTRL_L_ACTION_ID,
f1: true,
title: localize2('voidCtrlL', 'Void: Add Selection to Chat'),
title: localize2('voidCmdL', 'Void: Add Selection to Chat'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension
@@ -141,72 +89,119 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
// Get services
const commandService = accessor.get(ICommandService)
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
await commandService.executeCommand(VOID_ADD_SELECTION_TO_SIDEBAR_ACTION_ID)
}
})
const openNewThreadAndFireFocus = (accessor: ServicesAccessor) => {
const stateService = accessor.get(ISidebarStateService)
stateService.setState({ isHistoryOpen: false, currentTab: 'chat' })
const chatThreadService = accessor.get(IChatThreadService)
chatThreadService.openNewThread()
// focus
stateService.fireFocusChat()
const window = getActiveWindow()
window.requestAnimationFrame(() => stateService.fireFocusChat())
}
// New chat menu button
registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.newChatAction',
title: 'New Chat',
icon: { id: 'add' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const viewsService = accessor.get(IViewsService)
const metricsService = accessor.get(IMetricsService)
metricsService.capture('Chat Navigation', { type: 'New Chat' })
const editorService = accessor.get(ICodeEditorService)
const chatThreadService = accessor.get(IChatThreadService)
openNewThreadAndFireFocus(accessor)
metricsService.capture('Ctrl+L', {})
// capture selection and model before opening the chat panel
const editor = editorService.getActiveCodeEditor()
const model = editor?.getModel()
if (!model) return
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// open panel
const wasAlreadyOpen = viewsService.isViewContainerVisible(VOID_VIEW_CONTAINER_ID)
if (!wasAlreadyOpen) {
await commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
// Add selection to chat
// add line selection
if (selectionRange) {
editor?.setSelection({
startLineNumber: selectionRange.startLineNumber,
endLineNumber: selectionRange.endLineNumber,
startColumn: 1,
endColumn: Number.MAX_SAFE_INTEGER
})
chatThreadService.addNewStagingSelection({
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false },
})
}
// add file
else {
chatThreadService.addNewStagingSelection({
type: 'File',
uri: model.uri,
language: model.getLanguageId(),
state: { wasAddedAsCurrentFile: false },
})
}
await chatThreadService.focusCurrentChat()
}
})
// New chat keybind
// New chat keybind + menu button
const VOID_CMD_SHIFT_L_ACTION_ID = 'void.cmdShiftL'
registerAction2(class extends Action2 {
constructor() {
super({
id: 'void.newChatKeybindAction',
title: 'New Chat Keybind',
id: VOID_CMD_SHIFT_L_ACTION_ID,
title: 'New Chat',
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.KeyL,
weight: KeybindingWeight.VoidExtension,
},
icon: { id: 'add' },
menu: [{ id: MenuId.ViewTitle, group: 'navigation', when: ContextKeyExpr.equals('view', VOID_VIEW_ID), }],
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'New Chat Keybind' })
const chatThreadsService = accessor.get(IChatThreadService)
const editorService = accessor.get(ICodeEditorService)
metricsService.capture('Chat Navigation', { type: 'Start New Chat' })
openNewThreadAndFireFocus(accessor)
// get current selections and value to transfer
const oldThreadId = chatThreadsService.state.currentThreadId
const oldThread = chatThreadsService.state.allThreads[oldThreadId]
// add user's selection to chat
await commandService.executeCommand(VOID_CTRL_L_ACTION_ID)
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
const oldSelns = oldThread?.state.stagingSelections
const oldVal = oldUI?.textAreaRef?.current?.value
// open and focus new thread
chatThreadsService.openNewThread()
await chatThreadsService.focusCurrentChat()
// set new thread values
const newThreadId = chatThreadsService.state.currentThreadId
const newThread = chatThreadsService.state.allThreads[newThreadId]
const newUI = await newThread?.state.mountedInfo?.whenMounted
chatThreadsService.setCurrentThreadState({ stagingSelections: oldSelns, })
if (newUI?.textAreaRef?.current && oldVal) newUI.textAreaRef.current.value = oldVal
// if has selection, add it
const editor = editorService.getActiveCodeEditor()
const model = editor?.getModel()
if (!model) return
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
if (!selectionRange) return
editor?.setSelection({ startLineNumber: selectionRange.startLineNumber, endLineNumber: selectionRange.endLineNumber, startColumn: 1, endColumn: Number.MAX_SAFE_INTEGER })
chatThreadsService.addNewStagingSelection({
type: 'CodeSelection',
uri: model.uri,
language: model.getLanguageId(),
range: [selectionRange.startLineNumber, selectionRange.endLineNumber],
state: { wasAddedAsCurrentFile: false },
})
}
})
@@ -229,18 +224,12 @@ registerAction2(class extends Action2 {
return;
}
const stateService = accessor.get(ISidebarStateService)
const metricsService = accessor.get(IMetricsService)
const commandService = accessor.get(ICommandService)
metricsService.capture('Chat Navigation', { type: 'History' })
openNewThreadAndFireFocus(accessor)
// doesnt do anything right now
stateService.setState({ isHistoryOpen: !stateService.state.isHistoryOpen, currentTab: 'chat' })
stateService.fireBlurChat()
commandService.executeCommand(VOID_CMD_SHIFT_L_ACTION_ID)
}
})
@@ -265,28 +254,28 @@ registerAction2(class extends Action2 {
export class TabSwitchListener extends Disposable {
// export class TabSwitchListener extends Disposable {
constructor(
onSwitchTab: () => void,
@ICodeEditorService private readonly _editorService: ICodeEditorService,
) {
super()
// constructor(
// onSwitchTab: () => void,
// @ICodeEditorService private readonly _editorService: ICodeEditorService,
// ) {
// super()
// when editor switches tabs (models)
const addTabSwitchListeners = (editor: ICodeEditor) => {
this._register(editor.onDidChangeModel(e => {
if (e.newModelUrl?.scheme !== 'file') return
onSwitchTab()
}))
}
// // when editor switches tabs (models)
// const addTabSwitchListeners = (editor: ICodeEditor) => {
// this._register(editor.onDidChangeModel(e => {
// if (e.newModelUrl?.scheme !== 'file') return
// onSwitchTab()
// }))
// }
const initializeEditor = (editor: ICodeEditor) => {
addTabSwitchListeners(editor)
}
// const initializeEditor = (editor: ICodeEditor) => {
// addTabSwitchListeners(editor)
// }
// initialize current editors + any new editors
for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
}
}
// // initialize current editors + any new editors
// for (let editor of this._editorService.listCodeEditors()) initializeEditor(editor)
// this._register(this._editorService.onCodeEditorAdd(editor => { initializeEditor(editor) }))
// }
// }
@@ -1,82 +0,0 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Emitter, Event } from '../../../../base/common/event.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { ICommandService } from '../../../../platform/commands/common/commands.js';
import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { VOID_OPEN_SIDEBAR_ACTION_ID } from './sidebarPane.js';
// service that manages sidebar's state
export type VoidSidebarState = {
isHistoryOpen: boolean; // this isn't doing anything right now
currentTab: 'chat';
}
export interface ISidebarStateService {
readonly _serviceBrand: undefined;
readonly state: VoidSidebarState; // readonly to the user
setState(newState: Partial<VoidSidebarState>): void;
onDidChangeState: Event<void>;
onDidFocusChat: Event<void>;
onDidBlurChat: Event<void>;
fireFocusChat(): void;
fireBlurChat(): void;
}
export const ISidebarStateService = createDecorator<ISidebarStateService>('voidSidebarStateService');
class VoidSidebarStateService extends Disposable implements ISidebarStateService {
_serviceBrand: undefined;
static readonly ID = 'voidSidebarStateService';
private readonly _onDidChangeState = new Emitter<void>();
readonly onDidChangeState: Event<void> = this._onDidChangeState.event;
private readonly _onFocusChat = new Emitter<void>();
readonly onDidFocusChat: Event<void> = this._onFocusChat.event;
private readonly _onBlurChat = new Emitter<void>();
readonly onDidBlurChat: Event<void> = this._onBlurChat.event;
// state
state: VoidSidebarState
constructor(
@ICommandService private readonly commandService: ICommandService,
) {
super()
// initial state
this.state = { isHistoryOpen: false, currentTab: 'chat', }
}
setState(newState: Partial<VoidSidebarState>) {
// make sure view is open if the tab changes
if ('currentTab' in newState) {
this.commandService.executeCommand(VOID_OPEN_SIDEBAR_ACTION_ID)
}
this.state = { ...this.state, ...newState }
this._onDidChangeState.fire()
}
fireFocusChat() {
this._onFocusChat.fire()
}
fireBlurChat() {
this._onBlurChat.fire()
}
}
registerSingleton(ISidebarStateService, VoidSidebarStateService, InstantiationType.Eager);
@@ -5,6 +5,7 @@
import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js';
import { removeAnsiEscapeCodes } from '../../../../base/common/strings.js';
import { ITerminalCapabilityImplMap, TerminalCapability } from '../../../../platform/terminal/common/capabilities/capabilities.js';
import { URI } from '../../../../base/common/uri.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
@@ -13,6 +14,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { ITerminalService, ITerminalInstance, ICreateTerminalOptions } from '../../../../workbench/contrib/terminal/browser/terminal.js';
import { MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_CHARS, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js';
import { TerminalResolveReason } from '../common/toolsServiceTypes.js';
import { timeout } from '../../../../base/common/async.js';
@@ -20,28 +22,34 @@ export interface ITerminalToolService {
readonly _serviceBrand: undefined;
listPersistentTerminalIds(): string[];
runCommand(command: string, opts: { type: 'persistent', persistentTerminalId: string } | { type: 'ephemeral', cwd: string | null, terminalId: string }): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
runCommand(command: string, opts:
| { type: 'persistent', persistentTerminalId: string }
| { type: 'temporary', cwd: string | null, terminalId: string }
// | { type: 'apply', terminalId: string }
): Promise<{ interrupt: () => void; resPromise: Promise<{ result: string, resolveReason: TerminalResolveReason }> }>;
focusPersistentTerminal(terminalId: string): Promise<void>
persistentTerminalExists(terminalId: string): boolean
readTerminal(terminalId: string): Promise<string>
createPersistentTerminal(opts: { cwd: string | null }): Promise<string>
killPersistentTerminal(terminalId: string): Promise<void>
getPersistentTerminal(terminalId: string): ITerminalInstance | undefined
getTemporaryTerminal(terminalId: string): ITerminalInstance | undefined
}
export const ITerminalToolService = createDecorator<ITerminalToolService>('TerminalToolService');
function isCommandComplete(output: string) {
// https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
if (!completionMatch) { return false }
if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
return { exitCode: 0 }
}
// function isCommandComplete(output: string) {
// // https://code.visualstudio.com/docs/terminal/shell-integration#_vs-code-custom-sequences-osc-633-st
// const completionMatch = output.match(/\]633;D(?:;(\d+))?/)
// if (!completionMatch) { return false }
// if (completionMatch[1] !== undefined) return { exitCode: parseInt(completionMatch[1]) }
// return { exitCode: 0 }
// }
export const persistentTerminalNameOfId = (id: string) => {
@@ -114,32 +122,41 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'] }) {
const { cwd: override_cwd, config } = props
private async _createTerminal(props: { cwd: string | null, config: ICreateTerminalOptions['config'], hidden?: boolean }) {
const { cwd: override_cwd, config, hidden } = props;
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri
const cwd: URI | string | undefined = (override_cwd ?? undefined) ?? this.workspaceContextService.getWorkspace().folders[0]?.uri;
// create new terminal and return its ID
const terminal = await this.terminalService.createTerminal({
cwd: cwd,
location: TerminalLocation.Panel,
config: config,
})
const options: ICreateTerminalOptions = {
cwd,
location: hidden ? undefined : TerminalLocation.Panel,
config: {
name: config && 'name' in config ? config.name : undefined,
forceShellIntegration: true,
hideFromUser: hidden ? true : undefined,
// Copy any other properties from the provided config
...config,
},
// Skip profile check to ensure the terminal is created quickly
skipContributedProfileCheck: true,
};
// when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
const disposables: IDisposable[] = []
const waitForMount = new Promise<void>(res => {
let data = ''
const d = terminal.onData(newData => {
data += newData
if (isCommandComplete(data)) { res() }
})
disposables.push(d)
})
const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
const terminal = await this.terminalService.createTerminal(options)
await Promise.any([waitForMount, waitForTimeout,])
disposables.forEach(d => d.dispose())
// // when a new terminal is created, there is an initial command that gets run which is empty, wait for it to end before returning
// const disposables: IDisposable[] = []
// const waitForMount = new Promise<void>(res => {
// let data = ''
// const d = terminal.onData(newData => {
// data += newData
// if (isCommandComplete(data)) { res() }
// })
// disposables.push(d)
// })
// const waitForTimeout = new Promise<void>(res => { setTimeout(() => { res() }, 5000) })
// await Promise.any([waitForMount, waitForTimeout,])
// disposables.forEach(d => d.dispose())
return terminal
@@ -192,15 +209,64 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
readTerminal: ITerminalToolService['readTerminal'] = async (terminalId) => {
// Try persistent first, then temporary
const terminal = this.getPersistentTerminal(terminalId) ?? this.getTemporaryTerminal(terminalId);
if (!terminal) {
throw new Error(`Read Terminal: Terminal with ID ${terminalId} does not exist.`);
}
// Ensure the xterm.js instance has been created otherwise we cannot access the buffer.
if (!terminal.xterm) {
throw new Error('Read Terminal: The requested terminal has not yet been rendered and therefore has no scrollback buffer available.');
}
// Collect lines from the buffer iterator (oldest to newest)
const lines: string[] = [];
for (const line of terminal.xterm.getBufferReverseIterator()) {
lines.unshift(line);
}
let result = removeAnsiEscapeCodes(lines.join('\n'));
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2;
result = result.slice(0, half) + '\n...\n' + result.slice(result.length - half);
}
return result
};
private async _waitForCommandDetectionCapability(terminal: ITerminalInstance) {
const cmdCap = terminal.capabilities.get(TerminalCapability.CommandDetection);
if (cmdCap) return cmdCap
const disposables: IDisposable[] = []
const waitTimeout = timeout(10_000)
const waitForCapability = new Promise<ITerminalCapabilityImplMap[TerminalCapability.CommandDetection]>((res) => {
disposables.push(
terminal.capabilities.onDidAddCapability((e) => {
if (e.id === TerminalCapability.CommandDetection) res(e.capability)
})
)
})
const capability = await Promise.any([waitTimeout, waitForCapability])
.finally(() => { disposables.forEach((d) => d.dispose()) })
return capability ?? undefined
}
runCommand: ITerminalToolService['runCommand'] = async (command, params) => {
const { type } = params
await this.terminalService.whenConnected;
const { type } = params
const isPersistent = type === 'persistent'
let terminal: ITerminalInstance
const disposables: IDisposable[] = []
const isPersistent = type === 'persistent'
if (isPersistent) { // BG process
const { persistentTerminalId } = params
terminal = this.persistentTerminalInstanceOfId[persistentTerminalId];
@@ -208,7 +274,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
}
else {
const { cwd } = params
terminal = await this._createTerminal({ cwd: cwd, config: { name: 'Void Temporary Terminal', title: 'Void Temporary Terminal' } })
terminal = await this._createTerminal({ cwd: cwd, config: undefined, hidden: true })
this.temporaryTerminalInstanceOfId[params.terminalId] = terminal
}
@@ -216,6 +282,8 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
terminal.dispose()
if (!isPersistent)
delete this.temporaryTerminalInstanceOfId[params.terminalId]
else
delete this.persistentTerminalInstanceOfId[params.persistentTerminalId]
}
const waitForResult = async () => {
@@ -224,29 +292,29 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
this.terminalService.setActiveInstance(terminal)
await this.terminalService.focusActiveInstance()
}
let result: string = ''
let resolveReason: TerminalResolveReason | undefined = undefined
let resolveReason: TerminalResolveReason | undefined
// create this before we send so that we don't miss events on terminal
const waitUntilDone = new Promise<void>((res, rej) => {
const d2 = terminal.onData(async newData => {
if (resolveReason) return
result += newData
// onDone
const isDone = isCommandComplete(result)
if (isDone) {
resolveReason = { type: 'done', exitCode: isDone.exitCode }
res()
return
}
const cmdCap = await this._waitForCommandDetectionCapability(terminal)
// if (!cmdCap) throw new Error(`There was an error using the terminal: CommandDetection capability did not mount yet. Please try again in a few seconds or report this to the Void team.`)
// Prefer the structured command-detection capability when available
const waitUntilDone = new Promise<void>(resolve => {
if (!cmdCap) return
const l = cmdCap.onCommandFinished(cmd => {
if (resolveReason) return // already resolved
resolveReason = { type: 'done', exitCode: cmd.exitCode ?? 0 };
result = cmd.getOutput() ?? ''
l.dispose()
resolve()
})
disposables.push(d2)
disposables.push(l)
})
// send the command here
// send the command now that listeners are attached
await terminal.sendText(command, true)
const waitUntilInterrupt = isPersistent ?
@@ -277,18 +345,25 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
// wait for result
await Promise.any([waitUntilDone, waitUntilInterrupt])
.finally(() => disposables.forEach(d => d.dispose()))
// read result if timed out, since we didn't get it (could clean this code up but it's ok)
if (resolveReason?.type === 'timeout') {
const terminalId = isPersistent ? params.persistentTerminalId : params.terminalId
result = await this.readTerminal(terminalId)
}
disposables.forEach(d => d.dispose())
if (!isPersistent) {
interrupt()
}
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
if (!isPersistent) result = `$ ${command}\n${result}`
result = removeAnsiEscapeCodes(result)
.split('\n').slice(1, -1) // remove first and last line (first = command, last = andrewpareles/void %)
.join('\n')
// trim
if (result.length > MAX_TERMINAL_CHARS) {
const half = MAX_TERMINAL_CHARS / 2
result = result.slice(0, half)
@@ -8,28 +8,23 @@ 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 { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js'
import { LintErrorItem, BuiltinToolCallParams, BuiltinToolResultType, BuiltinToolName } from '../common/toolsServiceTypes.js'
import { IVoidModelService } from '../common/voidModelService.js'
import { EndOfLinePreference } from '../../../../editor/common/model.js'
import { IVoidCommandBarService } from './voidCommandBarService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js'
import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from '../common/directoryStrService.js'
import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js'
import { timeout } from '../../../../base/common/async.js'
import { RawToolParamsObj } from '../common/sendLLMMessageTypes.js'
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME, ToolName } from '../common/prompt/prompts.js'
import { MAX_CHILDREN_URIs_PAGE, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_BG_COMMAND_TIME, MAX_TERMINAL_INACTIVE_TIME } from '../common/prompt/prompts.js'
import { IVoidSettingsService } from '../common/voidSettingsService.js'
import { generateUuid } from '../../../../base/common/uuid.js'
// tool use for AI
type ValidateParams = { [T in ToolName]: (p: RawToolParamsObj) => ToolCallParams[T] }
type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T] | Promise<ToolResultType[T]>, interruptTool?: () => void }> }
type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: Awaited<ToolResultType[T]>) => string }
type ValidateBuiltinParams = { [T in BuiltinToolName]: (p: RawToolParamsObj) => BuiltinToolCallParams[T] }
type CallBuiltinTool = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T]) => Promise<{ result: BuiltinToolResultType[T] | Promise<BuiltinToolResultType[T]>, interruptTool?: () => void }> }
type BuiltinToolResultToString = { [T in BuiltinToolName]: (p: BuiltinToolCallParams[T], result: Awaited<BuiltinToolResultType[T]>) => string }
const isFalsy = (u: unknown) => {
@@ -44,12 +39,32 @@ const validateStr = (argName: string, value: unknown) => {
// We are NOT checking to make sure in workspace
// TODO!!!! check to make sure folder/file exists
const validateURI = (uriStr: unknown) => {
if (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`)
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Full value: ${JSON.stringify(uriStr)}.`)
const uri = URI.file(uriStr)
return uri
// Check if it's already a full URI with scheme (e.g., vscode-remote://, file://, etc.)
// Look for :// pattern which indicates a scheme is present
// Examples of supported URIs:
// - vscode-remote://wsl+Ubuntu/home/user/file.txt (WSL)
// - vscode-remote://ssh-remote+myserver/home/user/file.txt (SSH)
// - file:///home/user/file.txt (local file with scheme)
// - /home/user/file.txt (local file path, will be converted to file://)
// - C:\Users\file.txt (Windows local path, will be converted to file://)
if (uriStr.includes('://')) {
try {
const uri = URI.parse(uriStr)
return uri
} catch (e) {
// If parsing fails, it's a malformed URI
throw new Error(`Invalid URI format: ${uriStr}. Error: ${e}`)
}
} else {
// No scheme present, treat as file path
// This handles regular file paths like /home/user/file.txt or C:\Users\file.txt
const uri = URI.file(uriStr)
return uri
}
}
const validateOptionalURI = (uriStr: unknown) => {
@@ -111,9 +126,9 @@ const checkIfIsFolder = (uriStr: string) => {
export interface IToolsService {
readonly _serviceBrand: undefined;
validateParams: ValidateParams;
callTool: CallTool;
stringOfResult: ToolResultToString;
validateParams: ValidateBuiltinParams;
callTool: CallBuiltinTool;
stringOfResult: BuiltinToolResultToString;
}
export const IToolsService = createDecorator<IToolsService>('ToolsService');
@@ -122,9 +137,9 @@ export class ToolsService implements IToolsService {
readonly _serviceBrand: undefined;
public validateParams: ValidateParams;
public callTool: CallTool;
public stringOfResult: ToolResultToString;
public validateParams: ValidateBuiltinParams;
public callTool: CallBuiltinTool;
public stringOfResult: BuiltinToolResultToString;
constructor(
@IFileService fileService: IFileService,
@@ -139,7 +154,6 @@ export class ToolsService implements IToolsService {
@IMarkerService private readonly markerService: IMarkerService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
) {
const queryBuilder = instantiationService.createInstance(QueryBuilder);
this.validateParams = {
@@ -295,12 +309,14 @@ export class ToolsService implements IToolsService {
contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF)
}
const totalNumLines = model.getLineCount()
const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1)
const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1
const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate
const hasNextPage = (contents.length - 1) - toIdx >= 1
const totalFileLen = contents.length
return { result: { fileContents, totalFileLen, hasNextPage } }
return { result: { fileContents, totalFileLen, hasNextPage, totalNumLines } }
},
ls_dir: async ({ uri, pageNumber }) => {
@@ -399,7 +415,8 @@ export class ToolsService implements IToolsService {
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
editCodeService.instantlyApplyNewContent({ uri, newContent })
await editCodeService.callBeforeApplyOrEdit(uri)
editCodeService.instantlyRewriteFile({ uri, newContent })
// at end, get lint errors
const lintErrorsPromise = Promise.resolve().then(async () => {
await timeout(2000)
@@ -414,7 +431,7 @@ export class ToolsService implements IToolsService {
if (this.commandBarService.getStreamState(uri) === 'streaming') {
throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`)
}
console.log('aaaa', searchReplaceBlocks)
await editCodeService.callBeforeApplyOrEdit(uri)
editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks })
// at end, get lint errors
@@ -428,7 +445,7 @@ export class ToolsService implements IToolsService {
},
// ---
run_command: async ({ command, cwd, terminalId }) => {
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'ephemeral', cwd, terminalId })
const { resPromise, interrupt } = await this.terminalToolService.runCommand(command, { type: 'temporary', cwd, terminalId })
return { result: resPromise, interruptTool: interrupt }
},
run_persistent_command: async ({ command, persistentTerminalId }) => {
@@ -444,7 +461,6 @@ export class ToolsService implements IToolsService {
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
return { result: {} }
},
}
@@ -460,7 +476,7 @@ export class ToolsService implements IToolsService {
// given to the LLM after the call for successful tool calls
this.stringOfResult = {
read_file: (params, result) => {
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}`
return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `\nMore info because truncated: this file has ${result.totalNumLines} lines, or ${result.totalFileLen} characters.` : ''}`
},
ls_dir: (params, result) => {
const dirTreeStr = stringifyDirectoryTree1Deep(params, result)
@@ -522,7 +538,7 @@ export class ToolsService implements IToolsService {
}
// normal command
if (resolveReason.type === 'timeout') {
return `${result_}\nTerminal command ran, but was interrupted by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. To try with more time, open a persistent terminal and run the command there.`
return `${result_}\nTerminal command ran, but was automatically killed by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not finish successfully. To try with more time, open a persistent terminal and run the command there.`
}
throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`)
},
@@ -548,7 +564,6 @@ export class ToolsService implements IToolsService {
kill_persistent_terminal: (params, _result) => {
return `Successfully closed terminal "${params.persistentTerminalId}".`;
},
}
@@ -10,7 +10,6 @@ import './editCodeService.js'
// register Sidebar pane, state, actions (keybinds, menus) (Ctrl+L)
import './sidebarActions.js'
import './sidebarPane.js'
import './sidebarStateService.js'
// register quick edit (Ctrl+K)
import './quickEditActions.js'
@@ -56,6 +55,15 @@ import './tooltipService.js'
// register onboarding service
import './voidOnboardingService.js'
// register misc service
import './miscWokrbenchContrib.js'
// register file service (for explorer context menu)
import './fileService.js'
// register source control management
import './voidSCMService.js'
// ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ----------
// llmMessage
@@ -18,6 +18,16 @@ import { IEditCodeService } from './editCodeServiceInterface.js';
import { ITextModel } from '../../../../editor/common/model.js';
import { IModelService } from '../../../../editor/common/services/model.js';
import { generateUuid } from '../../../../base/common/uuid.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { VOID_ACCEPT_DIFF_ACTION_ID, VOID_REJECT_DIFF_ACTION_ID, VOID_GOTO_NEXT_DIFF_ACTION_ID, VOID_GOTO_PREV_DIFF_ACTION_ID, VOID_GOTO_NEXT_URI_ACTION_ID, VOID_GOTO_PREV_URI_ACTION_ID, VOID_ACCEPT_FILE_ACTION_ID, VOID_REJECT_FILE_ACTION_ID, VOID_ACCEPT_ALL_DIFFS_ACTION_ID, VOID_REJECT_ALL_DIFFS_ACTION_ID } from './actionIDs.js';
import { localize2 } from '../../../../nls.js';
import { KeybindingWeight } from '../../../../platform/keybinding/common/keybindingsRegistry.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { IMetricsService } from '../common/metricsService.js';
import { KeyMod } from '../../../../editor/common/services/editorBaseApi.js';
import { KeyCode } from '../../../../base/common/keyCodes.js';
import { ScrollType } from '../../../../editor/common/editorCommon.js';
import { IVoidModelService } from '../common/voidModelService.js';
@@ -33,6 +43,11 @@ export interface IVoidCommandBarService {
getStreamState: (uri: URI) => 'streaming' | 'idle-has-changes' | 'idle-no-changes';
setDiffIdx(uri: URI, newIdx: number | null): void;
getNextDiffIdx(step: 1 | -1): number | null;
getNextUriIdx(step: 1 | -1): number | null;
goToDiffIdx(idx: number | null): void;
goToURIIdx(idx: number | null): Promise<void>;
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }): void;
anyFileIsStreaming(): boolean;
@@ -85,6 +100,7 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
@ICodeEditorService private readonly _codeEditorService: ICodeEditorService,
@IModelService private readonly _modelService: IModelService,
@IEditCodeService private readonly _editCodeService: IEditCodeService,
@IVoidModelService private readonly _voidModelService: IVoidModelService,
) {
super();
@@ -164,10 +180,14 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
const newSortedDiffIds = this._computeSortedDiffs(newSortedDiffZoneIds)
const isStreaming = this._isAnyDiffZoneStreaming(currentDiffZones)
// When diffZones are added/removed, reset the diffIdx to 0 if we have diffs
const newDiffIdx = newSortedDiffIds.length > 0 ? 0 : null;
this._setState(uri, {
sortedDiffZoneIds: newSortedDiffZoneIds,
sortedDiffIds: newSortedDiffIds,
isStreaming: isStreaming
isStreaming: isStreaming,
diffIdx: newDiffIdx
})
this._onDidChangeState.fire({ uri })
}
@@ -182,9 +202,24 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
const currState = this.stateOfURI[uri.fsPath]
if (!currState) continue // should never happen
const { sortedDiffZoneIds } = currState
const oldSortedDiffIds = currState.sortedDiffIds;
const newSortedDiffIds = this._computeSortedDiffs(sortedDiffZoneIds)
// Handle diffIdx adjustment when diffs change
let newDiffIdx = currState.diffIdx;
// Check if diffs were removed
if (oldSortedDiffIds.length > newSortedDiffIds.length && currState.diffIdx !== null) {
// If currently selected diff was removed or we have fewer diffs than the current index
if (currState.diffIdx >= newSortedDiffIds.length) {
// Select the last diff if available, otherwise null
newDiffIdx = newSortedDiffIds.length > 0 ? newSortedDiffIds.length - 1 : null;
}
}
this._setState(uri, {
sortedDiffIds: newSortedDiffIds,
diffIdx: newDiffIdx
// sortedDiffZoneIds, // no change
// isStreaming, // no change
})
@@ -298,9 +333,9 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
}
// make sure diffIdx is always correct
if (newState.diffIdx && newState.diffIdx > newState.sortedDiffIds.length) {
if (newState.diffIdx !== null && newState.diffIdx > newState.sortedDiffIds.length) {
newState.diffIdx = newState.sortedDiffIds.length
if (newState.diffIdx < 0) newState.diffIdx = null
if (newState.diffIdx <= 0) newState.diffIdx = null
}
this.stateOfURI = {
@@ -347,6 +382,99 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar
return this.sortedURIs.some(uri => this.getStreamState(uri) === 'streaming')
}
getNextDiffIdx(step: 1 | -1): number | null {
// If no active URI, return null
if (!this.activeURI) return null;
const state = this.stateOfURI[this.activeURI.fsPath];
if (!state) return null;
const { diffIdx, sortedDiffIds } = state;
// If no diffs, return null
if (sortedDiffIds.length === 0) return null;
// Calculate next index with wrapping
const nextIdx = ((diffIdx ?? 0) + step + sortedDiffIds.length) % sortedDiffIds.length;
return nextIdx;
}
getNextUriIdx(step: 1 | -1): number | null {
// If no URIs with changes, return null
if (this.sortedURIs.length === 0) return null;
// If no active URI, return first or last based on step
if (!this.activeURI) {
return step === 1 ? 0 : this.sortedURIs.length - 1;
}
// Find current index
const currentIdx = this.sortedURIs.findIndex(uri => uri.fsPath === this.activeURI?.fsPath);
// If not found, return first or last based on step
if (currentIdx === -1) {
return step === 1 ? 0 : this.sortedURIs.length - 1;
}
// Calculate next index with wrapping
const nextIdx = (currentIdx + step + this.sortedURIs.length) % this.sortedURIs.length;
return nextIdx;
}
goToDiffIdx(idx: number | null): void {
// If null or no active URI, return
if (idx === null || !this.activeURI) return;
// Get state for the current URI
const state = this.stateOfURI[this.activeURI.fsPath];
if (!state) return;
const { sortedDiffIds } = state;
// Find the diff at the specified index
const diffid = sortedDiffIds[idx];
if (diffid === undefined) return;
// Get the diff object
const diff = this._editCodeService.diffOfId[diffid];
if (!diff) return;
// Find an active editor to focus
const editor = this._codeEditorService.getFocusedCodeEditor() ||
this._codeEditorService.getActiveCodeEditor();
if (!editor) return;
// Reveal the line in the editor
editor.revealLineNearTop(diff.startLine - 1, ScrollType.Immediate);
// Update the current diff index
this.setDiffIdx(this.activeURI, idx);
}
async goToURIIdx(idx: number | null): Promise<void> {
// If null or no URIs, return
if (idx === null || this.sortedURIs.length === 0) return;
// Get the URI at the specified index
const nextURI = this.sortedURIs[idx];
if (!nextURI) return;
// Get the model for this URI
const { model } = await this._voidModelService.getModelSafe(nextURI);
if (!model) return;
// Find an editor to use
const editor = this._codeEditorService.getFocusedCodeEditor() ||
this._codeEditorService.getActiveCodeEditor();
if (!editor) return;
// Open the URI in the editor
await this._codeEditorService.openCodeEditor(
{ resource: model.uri, options: { revealIfVisible: true } },
editor
);
}
acceptOrRejectAllFiles(opts: { behavior: 'reject' | 'accept' }) {
const { behavior } = opts
// if anything is streaming, do nothing
@@ -390,8 +518,8 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
// Style the container
// root.style.backgroundColor = 'rgb(248 113 113)';
root.style.height = '16rem'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
root.style.width = '16rem';
root.style.height = '256px'; // make a fixed size, and all contents go on the bottom right. this fixes annoying VS Code mounting issues
root.style.width = '100%';
root.style.flexDirection = 'column';
root.style.justifyContent = 'flex-end';
root.style.alignItems = 'flex-end';
@@ -439,3 +567,305 @@ class AcceptRejectAllFloatingWidget extends Widget implements IOverlayWidget {
}
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidAcceptDiffAction', 'Void: Accept Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Enter },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editCodeService = accessor.get(IEditCodeService);
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
if (!commandBarState) return;
const diffIdx = commandBarState.diffIdx ?? 0;
const diffid = commandBarState.sortedDiffIds[diffIdx];
if (!diffid) return;
metricsService.capture('Accept Diff', { diffid, keyboard: true });
editCodeService.acceptDiff({ diffid: parseInt(diffid) });
// After accepting the diff, navigate to the next diff
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx !== null) {
commandBarService.goToDiffIdx(nextDiffIdx);
}
}
});
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidRejectDiffAction', 'Void: Reject Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.Backspace },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const editCodeService = accessor.get(IEditCodeService);
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
const commandBarState = commandBarService.stateOfURI[activeURI.fsPath];
if (!commandBarState) return;
const diffIdx = commandBarState.diffIdx ?? 0;
const diffid = commandBarState.sortedDiffIds[diffIdx];
if (!diffid) return;
metricsService.capture('Reject Diff', { diffid, keyboard: true });
editCodeService.rejectDiff({ diffid: parseInt(diffid) });
// After rejecting the diff, navigate to the next diff
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx !== null) {
commandBarService.goToDiffIdx(nextDiffIdx);
}
}
});
// Go to next diff action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_NEXT_DIFF_ACTION_ID,
f1: true,
title: localize2('voidGoToNextDiffAction', 'Void: Go to Next Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.DownArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.DownArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const nextDiffIdx = commandBarService.getNextDiffIdx(1);
if (nextDiffIdx === null) return;
metricsService.capture('Navigate Diff', { direction: 'next', keyboard: true });
commandBarService.goToDiffIdx(nextDiffIdx);
}
});
// Go to previous diff action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_PREV_DIFF_ACTION_ID,
f1: true,
title: localize2('voidGoToPrevDiffAction', 'Void: Go to Previous Diff'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.UpArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.UpArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const prevDiffIdx = commandBarService.getNextDiffIdx(-1);
if (prevDiffIdx === null) return;
metricsService.capture('Navigate Diff', { direction: 'previous', keyboard: true });
commandBarService.goToDiffIdx(prevDiffIdx);
}
});
// Go to next URI action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_NEXT_URI_ACTION_ID,
f1: true,
title: localize2('voidGoToNextUriAction', 'Void: Go to Next File with Diffs'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.RightArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.RightArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const nextUriIdx = commandBarService.getNextUriIdx(1);
if (nextUriIdx === null) return;
metricsService.capture('Navigate URI', { direction: 'next', keyboard: true });
await commandBarService.goToURIIdx(nextUriIdx);
}
});
// Go to previous URI action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_GOTO_PREV_URI_ACTION_ID,
f1: true,
title: localize2('voidGoToPrevUriAction', 'Void: Go to Previous File with Diffs'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Alt | KeyMod.Shift | KeyCode.LeftArrow,
mac: { primary: KeyMod.WinCtrl | KeyMod.Alt | KeyCode.LeftArrow },
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
const prevUriIdx = commandBarService.getNextUriIdx(-1);
if (prevUriIdx === null) return;
metricsService.capture('Navigate URI', { direction: 'previous', keyboard: true });
await commandBarService.goToURIIdx(prevUriIdx);
}
});
// Accept current file action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_FILE_ACTION_ID,
f1: true,
title: localize2('voidAcceptFileAction', 'Void: Accept All Diffs in Current File'),
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Enter,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const editCodeService = accessor.get(IEditCodeService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
metricsService.capture('Accept File', { keyboard: true });
editCodeService.acceptOrRejectAllDiffAreas({
uri: activeURI,
behavior: 'accept',
removeCtrlKs: true
});
}
});
// Reject current file action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_FILE_ACTION_ID,
f1: true,
title: localize2('voidRejectFileAction', 'Void: Reject All Diffs in Current File'),
keybinding: {
primary: KeyMod.Alt | KeyMod.Shift | KeyCode.Backspace,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const editCodeService = accessor.get(IEditCodeService);
const metricsService = accessor.get(IMetricsService);
const activeURI = commandBarService.activeURI;
if (!activeURI) return;
metricsService.capture('Reject File', { keyboard: true });
editCodeService.acceptOrRejectAllDiffAreas({
uri: activeURI,
behavior: 'reject',
removeCtrlKs: true
});
}
});
// Accept all diffs in all files action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_ACCEPT_ALL_DIFFS_ACTION_ID,
f1: true,
title: localize2('voidAcceptAllDiffsAction', 'Void: Accept All Diffs in All Files'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Enter,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
if (commandBarService.anyFileIsStreaming()) return;
metricsService.capture('Accept All Files', { keyboard: true });
commandBarService.acceptOrRejectAllFiles({ behavior: 'accept' });
}
});
// Reject all diffs in all files action
registerAction2(class extends Action2 {
constructor() {
super({
id: VOID_REJECT_ALL_DIFFS_ACTION_ID,
f1: true,
title: localize2('voidRejectAllDiffsAction', 'Void: Reject All Diffs in All Files'),
keybinding: {
primary: KeyMod.CtrlCmd | KeyMod.Shift | KeyCode.Backspace,
weight: KeybindingWeight.VoidExtension,
}
});
}
async run(accessor: ServicesAccessor): Promise<void> {
const commandBarService = accessor.get(IVoidCommandBarService);
const metricsService = accessor.get(IMetricsService);
if (commandBarService.anyFileIsStreaming()) return;
metricsService.capture('Reject All Files', { keyboard: true });
commandBarService.acceptOrRejectAllFiles({ behavior: 'reject' });
}
});
@@ -0,0 +1,230 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ThemeIcon } from '../../../../base/common/themables.js'
import { localize2 } from '../../../../nls.js'
import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js'
import { ContextKeyExpr, IContextKey, IContextKeyService } from '../../../../platform/contextkey/common/contextkey.js'
import { ISCMService } from '../../scm/common/scm.js'
import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js'
import { IVoidSCMService } from '../common/voidSCMTypes.js'
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'
import { IVoidSettingsService } from '../common/voidSettingsService.js'
import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'
import { ILLMMessageService } from '../common/sendLLMMessageService.js'
import { ModelSelection, OverridesOfModel, ModelSelectionOptions } from '../common/voidSettingsTypes.js'
import { gitCommitMessage_systemMessage, gitCommitMessage_userMessage } from '../common/prompt/prompts.js'
import { LLMChatMessage } from '../common/sendLLMMessageTypes.js'
import { generateUuid } from '../../../../base/common/uuid.js'
import { ThrottledDelayer } from '../../../../base/common/async.js'
import { CancellationError, isCancellationError } from '../../../../base/common/errors.js'
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'
import { createDecorator, ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js'
import { Disposable } from '../../../../base/common/lifecycle.js'
import { INotificationService } from '../../../../platform/notification/common/notification.js'
interface ModelOptions {
modelSelection: ModelSelection | null
modelSelectionOptions?: ModelSelectionOptions
overridesOfModel: OverridesOfModel
}
export interface IGenerateCommitMessageService {
readonly _serviceBrand: undefined
generateCommitMessage(): Promise<void>
abort(): void
}
export const IGenerateCommitMessageService = createDecorator<IGenerateCommitMessageService>('voidGenerateCommitMessageService');
const loadingContextKey = 'voidSCMGenerateCommitMessageLoading'
class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService {
readonly _serviceBrand: undefined;
private readonly execute = new ThrottledDelayer(300)
private llmRequestId: string | null = null
private currentRequestId: string | null = null
private voidSCM: IVoidSCMService
private loadingContextKey: IContextKey<boolean>
constructor(
@ISCMService private readonly scmService: ISCMService,
@IMainProcessService mainProcessService: IMainProcessService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
@IConvertToLLMMessageService private readonly convertToLLMMessageService: IConvertToLLMMessageService,
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
@IContextKeyService private readonly contextKeyService: IContextKeyService,
@INotificationService private readonly notificationService: INotificationService
) {
super()
this.loadingContextKey = this.contextKeyService.createKey(loadingContextKey, false)
this.voidSCM = ProxyChannel.toService<IVoidSCMService>(mainProcessService.getChannel('void-channel-scm'))
}
override dispose() {
this.execute.dispose()
super.dispose()
}
async generateCommitMessage() {
this.loadingContextKey.set(true)
this.execute.trigger(async () => {
const requestId = generateUuid()
this.currentRequestId = requestId
try {
const { path, repo } = this.gitRepoInfo()
const [stat, sampledDiffs, branch, log] = await Promise.all([
this.voidSCM.gitStat(path),
this.voidSCM.gitSampledDiffs(path),
this.voidSCM.gitBranch(path),
this.voidSCM.gitLog(path)
])
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature['SCM'] ?? null
const modelSelectionOptions = modelSelection ? this.voidSettingsService.state.optionsOfModelSelection['SCM'][modelSelection?.providerName]?.[modelSelection.modelName] : undefined
const overridesOfModel = this.voidSettingsService.state.overridesOfModel
const modelOptions: ModelOptions = { modelSelection, modelSelectionOptions, overridesOfModel }
const prompt = gitCommitMessage_userMessage(stat, sampledDiffs, branch, log)
const simpleMessages = [{ role: 'user', content: prompt } as const]
const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({
simpleMessages,
systemMessage: gitCommitMessage_systemMessage,
modelSelection: modelOptions.modelSelection,
featureName: 'SCM',
})
const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions)
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
repo.input.setValue(commitMessage, false)
} catch (error) {
this.onError(error)
} finally {
if (this.isCurrentRequest(requestId)) {
this.loadingContextKey.set(false)
}
}
})
}
abort() {
if (this.llmRequestId) {
this.llmMessageService.abort(this.llmRequestId)
}
this.execute.cancel()
this.loadingContextKey.set(false)
this.currentRequestId = null
}
private gitRepoInfo() {
const repo = Array.from(this.scmService.repositories || []).find((r: any) => r.provider.contextValue === 'git')
if (!repo) { throw new Error('No git repository found') }
if (!repo.provider.rootUri?.fsPath) { throw new Error('No git repository root path found') }
return { path: repo.provider.rootUri.fsPath, repo }
}
/** LLM Functions */
private sendLLMMessage(messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions): Promise<string> {
return new Promise((resolve, reject) => {
this.llmRequestId = this.llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
messages,
separateSystemMessage,
chatMode: null,
modelSelection: modelOptions.modelSelection,
modelSelectionOptions: modelOptions.modelSelectionOptions,
overridesOfModel: modelOptions.overridesOfModel,
onText: () => { },
onFinalMessage: (params: { fullText: string }) => {
const match = params.fullText.match(/<output>([\s\S]*?)<\/output>/i)
const commitMessage = match ? match[1].trim() : ''
resolve(commitMessage)
},
onError: (error) => {
console.error(error)
reject(error)
},
onAbort: () => {
reject(new CancellationError())
},
logging: { loggingName: 'VoidSCM - Commit Message' },
})
})
}
/** Request Helpers */
private isCurrentRequest(requestId: string) {
return requestId === this.currentRequestId
}
/** UI Functions */
private onError(error: any) {
if (!isCancellationError(error)) {
console.error(error)
this.notificationService.error(localize2('voidFailedToGenerateCommitMessage', 'Failed to generate commit message.').value)
}
}
}
class GenerateCommitMessageAction extends Action2 {
constructor() {
super({
id: 'void.generateCommitMessageAction',
title: localize2('voidCommitMessagePrompt', 'Void: Generate Commit Message'),
icon: ThemeIcon.fromId('sparkle'),
tooltip: localize2('voidCommitMessagePromptTooltip', 'Void: Generate Commit Message'),
f1: true,
menu: [{
id: MenuId.SCMInputBox,
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, false)),
group: 'inline'
}]
})
}
async run(accessor: ServicesAccessor): Promise<void> {
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
generateCommitMessageService.generateCommitMessage()
}
}
class LoadingGenerateCommitMessageAction extends Action2 {
constructor() {
super({
id: 'void.loadingGenerateCommitMessageAction',
title: localize2('voidCommitMessagePromptCancel', 'Void: Cancel Commit Message Generation'),
icon: ThemeIcon.fromId('stop-circle'),
tooltip: localize2('voidCommitMessagePromptCancelTooltip', 'Void: Cancel Commit Message Generation'),
f1: false, //Having a cancel command in the command palette is more confusing than useful.
menu: [{
id: MenuId.SCMInputBox,
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, true)),
group: 'inline'
}]
})
}
async run(accessor: ServicesAccessor): Promise<void> {
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
generateCommitMessageService.abort()
}
}
registerAction2(GenerateCommitMessageAction)
registerAction2(LoadingGenerateCommitMessageAction)
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed)
@@ -8,7 +8,7 @@ import Severity from '../../../../base/common/severity.js';
import { ServicesAccessor } from '../../../../editor/browser/editorExtensions.js';
import { localize2 } from '../../../../nls.js';
import { Action2, registerAction2 } from '../../../../platform/actions/common/actions.js';
import { INotificationActions, INotificationService } from '../../../../platform/notification/common/notification.js';
import { INotificationActions, INotificationHandle, INotificationService } from '../../../../platform/notification/common/notification.js';
import { IMetricsService } from '../common/metricsService.js';
import { IVoidUpdateService } from '../common/voidUpdateService.js';
import { IWorkbenchContribution, registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js';
@@ -20,7 +20,7 @@ import { IAction } from '../../../../base/common/actions.js';
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService) => {
const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifService: INotificationService, updateService: IUpdateService): INotificationHandle => {
const message = res?.message || 'This is a very old version of Void, please download the latest version! [Void Editor](https://voideditor.com/download-beta)!'
let actions: INotificationActions | undefined
@@ -119,18 +119,21 @@ const notifyUpdate = (res: VoidCheckUpdateRespose & { message: string }, notifSe
progress: actions ? { worked: 0, total: 100 } : undefined,
actions: actions,
})
return notifController
// const d = notifController.onDidClose(() => {
// notifyYesUpdate(notifService, res)
// d.dispose()
// })
}
const notifyErrChecking = (notifService: INotificationService) => {
const notifyErrChecking = (notifService: INotificationService): INotificationHandle => {
const message = `Void Error: There was an error checking for updates. If this persists, please get in touch or reinstall Void [here](https://voideditor.com/download-beta)!`
notifService.notify({
const notifController = notifService.notify({
severity: Severity.Info,
message: message,
sticky: true,
})
return notifController
}
@@ -140,30 +143,35 @@ const performVoidCheck = async (
voidUpdateService: IVoidUpdateService,
metricsService: IMetricsService,
updateService: IUpdateService,
) => {
): Promise<INotificationHandle | null> => {
const metricsTag = explicit ? 'Manual' : 'Auto'
metricsService.capture(`Void Update ${metricsTag}: Checking...`, {})
const res = await voidUpdateService.check(explicit)
if (!res) {
notifyErrChecking(notifService);
const notifController = notifyErrChecking(notifService);
metricsService.capture(`Void Update ${metricsTag}: Error`, { res })
return notifController
}
else {
if (res.message) {
notifyUpdate(res, notifService, updateService)
const notifController = notifyUpdate(res, notifService, updateService)
metricsService.capture(`Void Update ${metricsTag}: Yes`, { res })
return notifController
}
else {
metricsService.capture(`Void Update ${metricsTag}: No`, { res })
return
return null
}
}
}
// Action
let lastNotifController: INotificationHandle | null = null
registerAction2(class extends Action2 {
constructor() {
super({
@@ -177,7 +185,15 @@ registerAction2(class extends Action2 {
const notifService = accessor.get(INotificationService)
const metricsService = accessor.get(IMetricsService)
const updateService = accessor.get(IUpdateService)
performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
const currNotifController = lastNotifController
const newController = await performVoidCheck(true, notifService, voidUpdateService, metricsService, updateService)
if (newController) {
currNotifController?.close()
lastNotifController = newController
}
}
})
@@ -5,31 +5,32 @@
import { URI } from '../../../../base/common/uri.js';
import { VoidFileSnapshot } from './editCodeServiceTypes.js';
import { ToolName } from './prompt/prompts.js';
import { AnthropicReasoning, RawToolParamsObj } from './sendLLMMessageTypes.js';
import { ToolCallParams, ToolResultType } from './toolsServiceTypes.js';
import { ToolCallParams, ToolName, ToolResult } from './toolsServiceTypes.js';
export type ToolMessage<T extends ToolName> = {
role: 'tool';
content: string; // give this result to LLM (string of value)
id: string;
rawParams: RawToolParamsObj;
mcpServerName: string | undefined; // the server name at the time of the call
} & (
// in order of events:
| { type: 'invalid_params', result: null, name: T, }
| { type: 'tool_request', result: null, name: T, params: ToolCallParams[T], } // params were validated, awaiting user
| { type: 'tool_request', result: null, name: T, params: ToolCallParams<T>, } // params were validated, awaiting user
| { type: 'running_now', result: null, name: T, params: ToolCallParams[T], }
| { type: 'running_now', result: null, name: T, params: ToolCallParams<T>, }
| { type: 'tool_error', result: string, name: T, params: ToolCallParams[T], } // error when tool was running
| { type: 'success', result: Awaited<ToolResultType[T]>, name: T, params: ToolCallParams[T], }
| { type: 'rejected', result: null, name: T, params: ToolCallParams[T] }
| { type: 'tool_error', result: string, name: T, params: ToolCallParams<T>, } // error when tool was running
| { type: 'success', result: Awaited<ToolResult<T>>, name: T, params: ToolCallParams<T>, }
| { type: 'rejected', result: null, name: T, params: ToolCallParams<T> }
) // user rejected
export type DecorativeCanceledTool = {
role: 'interrupted_streaming_tool';
name: ToolName;
mcpServerName: string | undefined; // the server name at the time of the call
}
@@ -7,29 +7,28 @@ import { URI } from '../../../../base/common/uri.js';
import { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js';
import { IExplorerService } from '../../files/browser/files.js';
import { SortOrder } from '../../files/common/files.js';
import { ExplorerItem } from '../../files/common/explorerModel.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from '../common/prompt/prompts.js';
import { ShallowDirectoryItem, BuiltinToolCallParams, BuiltinToolResultType } from './toolsServiceTypes.js';
import { MAX_CHILDREN_URIs_PAGE, MAX_DIRSTR_CHARS_TOTAL_BEGINNING, MAX_DIRSTR_CHARS_TOTAL_TOOL } from './prompt/prompts.js';
const MAX_FILES_TOTAL = 300;
const MAX_FILES_TOTAL = 1000;
const DEFAULT_MAX_DEPTH = 3;
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
const START_MAX_DEPTH = Infinity;
const START_MAX_ITEMS_PER_DIR = Infinity; // Add start value as Infinity
const DEFAULT_MAX_DEPTH = 3;
const DEFAULT_MAX_ITEMS_PER_DIR = 3;
export interface IDirectoryStrService {
readonly _serviceBrand: undefined;
getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string, maxItemsPerDir?: number }): Promise<string>
getDirectoryStrTool(uri: URI): Promise<string>
getAllDirectoriesStr(opts: { cutOffMessage: string }): Promise<string>
getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]>
}
export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidDirectoryStrService');
@@ -38,35 +37,35 @@ export const IDirectoryStrService = createDecorator<IDirectoryStrService>('voidD
// Check if it's a known filtered type like .git
const shouldExcludeDirectory = (item: ExplorerItem) => {
if (item.name === '.git' ||
item.name === 'node_modules' ||
item.name.startsWith('.') ||
item.name === 'dist' ||
item.name === 'build' ||
item.name === 'out' ||
item.name === 'bin' ||
item.name === 'coverage' ||
item.name === '__pycache__' ||
item.name === 'env' ||
item.name === 'venv' ||
item.name === 'tmp' ||
item.name === 'temp' ||
item.name === 'artifacts' ||
item.name === 'target' ||
item.name === 'obj' ||
item.name === 'vendor' ||
item.name === 'logs' ||
item.name === 'cache' ||
item.name === 'resource' ||
item.name === 'resources'
const shouldExcludeDirectory = (name: string) => {
if (name === '.git' ||
name === 'node_modules' ||
name.startsWith('.') ||
name === 'dist' ||
name === 'build' ||
name === 'out' ||
name === 'bin' ||
name === 'coverage' ||
name === '__pycache__' ||
name === 'env' ||
name === 'venv' ||
name === 'tmp' ||
name === 'temp' ||
name === 'artifacts' ||
name === 'target' ||
name === 'obj' ||
name === 'vendor' ||
name === 'logs' ||
name === 'cache' ||
name === 'resource' ||
name === 'resources'
) {
return true;
}
if (item.name.match(/\bout\b/)) return true
if (item.name.match(/\bbuild\b/)) return true
if (name.match(/\bout\b/)) return true
if (name.match(/\bbuild\b/)) return true
return false;
}
@@ -77,7 +76,7 @@ export const computeDirectoryTree1Deep = async (
fileService: IFileService,
rootURI: URI,
pageNumber: number = 1,
): Promise<ToolResultType['ls_dir']> => {
): Promise<BuiltinToolResultType['ls_dir']> => {
const stat = await fileService.resolve(rootURI, { resolveMetadata: false });
if (!stat.isDirectory) {
return { children: null, hasNextPage: false, hasPrevPage: false, itemsRemaining: 0 };
@@ -108,7 +107,7 @@ export const computeDirectoryTree1Deep = async (
};
};
export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], result: ToolResultType['ls_dir']): string => {
export const stringifyDirectoryTree1Deep = (params: BuiltinToolCallParams['ls_dir'], result: BuiltinToolResultType['ls_dir']): string => {
if (!result.children) {
return `Error: ${params.uri} is not a directory`;
}
@@ -138,10 +137,16 @@ export const stringifyDirectoryTree1Deep = (params: ToolCallParams['ls_dir'], re
// ---------- IN GENERAL ----------
const resolveChildren = async (children: undefined | IFileStat[], fileService: IFileService): Promise<IFileStat[]> => {
const res = await fileService.resolveAll(children ?? [])
const stats = res.map(s => s.success ? s.stat : null).filter(s => !!s)
return stats
}
// Remove the old computeDirectoryTree function and replace with a combined version that handles both computation and rendering
const computeAndStringifyDirectoryTree = async (
eItem: ExplorerItem,
explorerService: IExplorerService,
eItem: IFileStat,
fileService: IFileService,
MAX_CHARS: number,
fileCount: { count: number } = { count: 0 },
options: { maxDepth?: number, currentDepth?: number, maxItemsPerDir?: number } = {}
@@ -181,12 +186,13 @@ const computeAndStringifyDirectoryTree = async (
let remainingChars = MAX_CHARS - nodeLine.length;
// Check if it's a directory we should skip
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem);
const isGitIgnoredDirectory = eItem.isDirectory && shouldExcludeDirectory(eItem.name);
// Fetch and process children if not a filtered directory
if (eItem.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await eItem.fetchChildren(SortOrder.Modified);
const eChildren = await resolveChildren(eItem.children, fileService)
// Then recursively add all children with proper tree formatting
if (eChildren && eChildren.length > 0) {
@@ -194,7 +200,7 @@ const computeAndStringifyDirectoryTree = async (
eChildren,
remainingChars,
'',
explorerService,
fileService,
fileCount,
{ maxDepth, currentDepth, maxItemsPerDir } // Pass maxItemsPerDir to the render function
);
@@ -208,10 +214,10 @@ const computeAndStringifyDirectoryTree = async (
// Helper function to render children with proper tree formatting
const renderChildrenCombined = async (
children: ExplorerItem[],
children: IFileStat[],
maxChars: number,
parentPrefix: string,
explorerService: IExplorerService,
fileService: IFileService,
fileCount: { count: number },
options: { maxDepth: number, currentDepth: number, maxItemsPerDir?: number }
): Promise<{ childrenContent: string, childrenCutOff: boolean }> => {
@@ -263,12 +269,12 @@ const renderChildrenCombined = async (
const nextLevelPrefix = parentPrefix + (isLast ? ' ' : '│ ');
// Skip processing children for git ignored directories
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child);
const isGitIgnoredDirectory = child.isDirectory && shouldExcludeDirectory(child.name);
// Create the prefix for the next level (continuation line or space)
if (child.isDirectory && !isGitIgnoredDirectory) {
// Fetch children with Modified sort order to show recently modified first
const eChildren = await child.fetchChildren(SortOrder.Modified);
const eChildren = await resolveChildren(child.children, fileService)
if (eChildren && eChildren.length > 0) {
const {
@@ -278,7 +284,7 @@ const renderChildrenCombined = async (
eChildren,
remainingChars,
nextLevelPrefix,
explorerService,
fileService,
fileCount,
{ maxDepth, currentDepth: nextDepth, maxItemsPerDir }
);
@@ -311,7 +317,68 @@ const renderChildrenCombined = async (
};
// ---------------------------------------------------
// ------------------------- FOLDERS -------------------------
export async function getAllUrisInDirectory(
directoryUri: URI,
maxResults: number,
fileService: IFileService,
): Promise<URI[]> {
const result: URI[] = [];
// Helper function to recursively collect URIs
async function visitAll(folderStat: IFileStat): Promise<boolean> {
// Stop if we've reached the limit
if (result.length >= maxResults) {
return false;
}
try {
if (!folderStat.isDirectory || !folderStat.children) {
return true;
}
const eChildren = await resolveChildren(folderStat.children, fileService)
// Process files first (common convention to list files before directories)
for (const child of eChildren) {
if (!child.isDirectory) {
result.push(child.resource);
// Check if we've hit the limit
if (result.length >= maxResults) {
return false;
}
}
}
// Then process directories recursively
for (const child of eChildren) {
const isGitIgnored = shouldExcludeDirectory(child.name)
if (child.isDirectory && !isGitIgnored) {
const shouldContinue = await visitAll(child);
if (!shouldContinue) {
return false;
}
}
}
return true;
} catch (error) {
console.error(`Error processing directory ${folderStat.resource.fsPath}: ${error}`);
return true; // Continue despite errors in a specific directory
}
}
const rootStat = await fileService.resolve(directoryUri)
await visitAll(rootStat);
return result;
}
// --------------------------------------------------
class DirectoryStrService extends Disposable implements IDirectoryStrService {
@@ -319,21 +386,25 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
constructor(
@IWorkspaceContextService private readonly workspaceContextService: IWorkspaceContextService,
@IExplorerService private readonly explorerService: IExplorerService,
@IFileService private readonly fileService: IFileService,
) {
super();
}
async getDirectoryStrTool(uri: URI, options?: { maxItemsPerDir?: number }) {
const eRoot = this.explorerService.findClosest(uri)
async getAllURIsInDirectory(uri: URI, opts: { maxResults: number }): Promise<URI[]> {
return getAllUrisInDirectory(uri, opts.maxResults, this.fileService)
}
async getDirectoryStrTool(uri: URI) {
const eRoot = await this.fileService.resolve(uri)
if (!eRoot) throw new Error(`The folder ${uri.fsPath} does not exist.`)
const maxItemsPerDir = options?.maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
const maxItemsPerDir = START_MAX_ITEMS_PER_DIR; // Use START_MAX_ITEMS_PER_DIR
// First try with START_MAX_DEPTH
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir }
@@ -344,7 +415,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_TOOL,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
@@ -363,7 +434,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
return c
}
async getAllDirectoriesStr({ cutOffMessage, maxItemsPerDir }: { cutOffMessage: string, maxItemsPerDir?: number }) {
async getAllDirectoriesStr({ cutOffMessage, }: { cutOffMessage: string, }) {
let str: string = '';
let cutOff = false;
const folders = this.workspaceContextService.getWorkspace().folders;
@@ -371,7 +442,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
return '(NO WORKSPACE OPEN)';
// Use START_MAX_ITEMS_PER_DIR if not specified
const startMaxItemsPerDir = maxItemsPerDir ?? START_MAX_ITEMS_PER_DIR;
const startMaxItemsPerDir = START_MAX_ITEMS_PER_DIR;
for (let i = 0; i < folders.length; i += 1) {
if (i > 0) str += '\n';
@@ -381,13 +452,13 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
str += `Directory of ${f.uri.fsPath}:\n`;
const rootURI = f.uri;
const eRoot = this.explorerService.findClosestRoot(rootURI);
const eRoot = await this.fileService.resolve(rootURI)
if (!eRoot) continue;
// First try with START_MAX_DEPTH and startMaxItemsPerDir
const { content: initialContent, wasCutOff: initialCutOff } = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: START_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: startMaxItemsPerDir }
@@ -398,7 +469,7 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
if (initialCutOff) {
const result = await computeAndStringifyDirectoryTree(
eRoot,
this.explorerService,
this.fileService,
MAX_DIRSTR_CHARS_TOTAL_BEGINNING - str.length,
{ count: 0 },
{ maxDepth: DEFAULT_MAX_DEPTH, currentDepth: 0, maxItemsPerDir: DEFAULT_MAX_ITEMS_PER_DIR }
@@ -417,11 +488,8 @@ class DirectoryStrService extends Disposable implements IDirectoryStrService {
}
}
if (cutOff) {
return `${str.trimEnd()}\n${cutOffMessage}`
}
return str
const ans = cutOff ? `${str.trimEnd()}\n${cutOffMessage}` : str
return ans
}
}
@@ -1,8 +1,40 @@
export const acceptBg = '#1a7431'
export const acceptAllBg = '#1e8538'
export const acceptBorder = '1px solid #145626'
export const rejectBg = '#b42331'
export const rejectAllBg = '#cf2838'
export const rejectBorder = '1px solid #8e1c27'
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { Color, RGBA } from '../../../../../base/common/color.js';
import { registerColor } from '../../../../../platform/theme/common/colorUtils.js';
// editCodeService colors
const sweepBG = new Color(new RGBA(100, 100, 100, .2));
const highlightBG = new Color(new RGBA(100, 100, 100, .1));
const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5));
const acceptBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2)
const rejectBG = new Color(new RGBA(255, 0, 0, .1)); // default is RGBA(255, 0, 0, .2)
// Widget colors
export const acceptAllBg = 'rgb(30, 133, 56)'
export const acceptBg = 'rgb(26, 116, 48)'
export const acceptBorder = '1px solid rgb(20, 86, 38)'
export const rejectAllBg = 'rgb(207, 40, 56)'
export const rejectBg = 'rgb(180, 35, 49)'
export const rejectBorder = '1px solid rgb(142, 28, 39)'
export const buttonFontSize = '11px'
export const buttonTextColor = 'white'
const configOfBG = (color: Color) => {
return { dark: color, light: color, hcDark: color, hcLight: color, }
}
// gets converted to --vscode-void-greenBG, see void.css, asCssVariable
registerColor('void.greenBG', configOfBG(acceptBG), '', true);
registerColor('void.redBG', configOfBG(rejectBG), '', true);
registerColor('void.sweepBG', configOfBG(sweepBG), '', true);
registerColor('void.highlightBG', configOfBG(highlightBG), '', true);
registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true);
@@ -0,0 +1,360 @@
/*--------------------------------------------------------------------------------------
* 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 { Disposable } from '../../../../base/common/lifecycle.js';
import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js';
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IFileService } from '../../../../platform/files/common/files.js';
import { IPathService } from '../../../services/path/common/pathService.js';
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { IProductService } from '../../../../platform/product/common/productService.js';
import { VSBuffer } from '../../../../base/common/buffer.js';
import { IChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js';
import { MCPServerOfName, MCPConfigFileJSON, MCPServer, MCPToolCallParams, RawMCPToolCall, MCPServerEventResponse } from './mcpServiceTypes.js';
import { Event, Emitter } from '../../../../base/common/event.js';
import { InternalToolInfo } from './prompt/prompts.js';
import { IVoidSettingsService } from './voidSettingsService.js';
import { MCPUserStateOfName } from './voidSettingsTypes.js';
type MCPServiceState = {
mcpServerOfName: MCPServerOfName,
error: string | undefined, // global parsing error
}
export interface IMCPService {
readonly _serviceBrand: undefined;
revealMCPConfigFile(): Promise<void>;
toggleServerIsOn(serverName: string, isOn: boolean): Promise<void>;
readonly state: MCPServiceState; // NOT persisted
onDidChangeState: Event<void>;
getMCPTools(): InternalToolInfo[] | undefined;
callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }>;
stringifyResult(result: RawMCPToolCall): string
}
export const IMCPService = createDecorator<IMCPService>('mcpConfigService');
const MCP_CONFIG_FILE_NAME = 'mcp.json';
const MCP_CONFIG_SAMPLE = { mcpServers: {} }
const MCP_CONFIG_SAMPLE_STRING = JSON.stringify(MCP_CONFIG_SAMPLE, null, 2);
// export interface MCPCallToolOfToolName {
// [toolName: string]: (params: any) => Promise<{
// result: any | Promise<any>,
// interruptTool?: () => void
// }>;
// }
class MCPService extends Disposable implements IMCPService {
_serviceBrand: undefined;
private readonly channel: IChannel // MCPChannel
// list of MCP servers pulled from mcpChannel
state: MCPServiceState = {
mcpServerOfName: {},
error: undefined,
}
// Emitters for server events
private readonly _onDidChangeState = new Emitter<void>();
public readonly onDidChangeState = this._onDidChangeState.event;
// private readonly _onLoadingServersChange = new Emitter<MCPServerEventLoadingParam>();
// public readonly onLoadingServersChange = this._onLoadingServersChange.event;
constructor(
@IFileService private readonly fileService: IFileService,
@IPathService private readonly pathService: IPathService,
@IProductService private readonly productService: IProductService,
@IEditorService private readonly editorService: IEditorService,
@IMainProcessService private readonly mainProcessService: IMainProcessService,
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
) {
super();
this.channel = this.mainProcessService.getChannel('void-channel-mcp')
const onEvent = (e: MCPServerEventResponse) => {
// console.log('GOT EVENT', e)
this._setMCPServerState(e.response.name, e.response.newServer)
}
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPServerEventResponse>)(onEvent));
this._register((this.channel.listen('onUpdate_server') satisfies Event<MCPServerEventResponse>)(onEvent));
this._register((this.channel.listen('onDelete_server') satisfies Event<MCPServerEventResponse>)(onEvent));
this._initialize();
}
private async _initialize() {
try {
await this.voidSettingsService.waitForInitState;
// Create .mcpConfig if it doesn't exist
const mcpConfigUri = await this._getMCPConfigFilePath();
const fileExists = await this._configFileExists(mcpConfigUri);
if (!fileExists) {
await this._createMCPConfigFile(mcpConfigUri);
console.log('MCP Config file created:', mcpConfigUri.toString());
}
await this._addMCPConfigFileWatcher();
await this._refreshMCPServers();
} catch (error) {
console.error('Error initializing MCPService:', error);
}
}
private readonly _setMCPServerState = async (serverName: string, newServer: MCPServer | undefined) => {
if (newServer === undefined) {
// Remove the server from the state
const { [serverName]: removed, ...remainingServers } = this.state.mcpServerOfName;
this.state = {
...this.state,
mcpServerOfName: remainingServers
}
} else {
// Add or update the server
this.state = {
...this.state,
mcpServerOfName: {
...this.state.mcpServerOfName,
[serverName]: newServer
}
}
}
this._onDidChangeState.fire();
}
private readonly _setHasError = async (errMsg: string | undefined) => {
this.state = {
...this.state,
error: errMsg,
}
this._onDidChangeState.fire();
}
// Create the file/directory if it doesn't exist
private async _createMCPConfigFile(mcpConfigUri: URI): Promise<void> {
await this.fileService.createFile(mcpConfigUri.with({ path: mcpConfigUri.path }));
const buffer = VSBuffer.fromString(MCP_CONFIG_SAMPLE_STRING);
await this.fileService.writeFile(mcpConfigUri, buffer);
}
private async _addMCPConfigFileWatcher(): Promise<void> {
const mcpConfigUri = await this._getMCPConfigFilePath();
this._register(
this.fileService.watch(mcpConfigUri)
)
this._register(this.fileService.onDidFilesChange(async e => {
if (!e.contains(mcpConfigUri)) return
await this._refreshMCPServers();
}));
}
// Client-side functions
public async revealMCPConfigFile(): Promise<void> {
try {
const mcpConfigUri = await this._getMCPConfigFilePath();
await this.editorService.openEditor({
resource: mcpConfigUri,
options: {
pinned: true,
revealIfOpened: true,
}
});
} catch (error) {
console.error('Error opening MCP config file:', error);
}
}
public getMCPTools(): InternalToolInfo[] | undefined {
const allTools: InternalToolInfo[] = []
for (const serverName in this.state.mcpServerOfName) {
const server = this.state.mcpServerOfName[serverName];
server.tools?.forEach(tool => {
allTools.push({
description: tool.description || '',
params: this._transformInputSchemaToParams(tool.inputSchema),
name: tool.name,
mcpServerName: serverName,
})
})
}
if (allTools.length === 0) return undefined
return allTools
}
private _transformInputSchemaToParams(inputSchema?: Record<string, any>): { [paramName: string]: { description: string } } {
// Check if inputSchema is valid
if (!inputSchema || !inputSchema.properties) return {};
const params: { [paramName: string]: { description: string } } = {};
Object.keys(inputSchema.properties).forEach(paramName => {
const propertyValues = inputSchema.properties[paramName];
// Check if propertyValues is not an object
if (typeof propertyValues !== 'object') {
console.warn(`Invalid property value for ${paramName}: expected object, got ${typeof propertyValues}`);
return; // in forEach the return is equivalent to continue
}
// Add the parameter to the params object
params[paramName] = {
description: JSON.stringify(propertyValues.description || '', null, 2) || '',
}
});
return params;
}
private async _getMCPConfigFilePath(): Promise<URI> {
const appName = this.productService.dataFolderName
const userHome = await this.pathService.userHome();
const uri = URI.joinPath(userHome, appName, MCP_CONFIG_FILE_NAME)
return uri
}
private async _configFileExists(mcpConfigUri: URI): Promise<boolean> {
try {
await this.fileService.stat(mcpConfigUri);
return true;
} catch (error) {
return false;
}
}
private async _parseMCPConfigFile(): Promise<MCPConfigFileJSON | null> {
const mcpConfigUri = await this._getMCPConfigFilePath();
try {
const fileContent = await this.fileService.readFile(mcpConfigUri);
const contentString = fileContent.value.toString();
const configFileJson = JSON.parse(contentString);
if (!configFileJson.mcpServers) {
throw new Error('Missing mcpServers property');
}
return configFileJson as MCPConfigFileJSON;
} catch (error) {
const fullError = `Error parsing MCP config file: ${error}`;
this._setHasError(fullError)
return null;
}
}
// Handle server state changes
private async _refreshMCPServers(): Promise<void> {
this._setHasError(undefined)
const newConfigFileJSON = await this._parseMCPConfigFile();
if (!newConfigFileJSON) { console.log(`Not setting state: MCP config file not found`); return }
if (!newConfigFileJSON?.mcpServers) { console.log(`Not setting state: MCP config file did not have an 'mcpServers' field`); return }
const oldConfigFileNames = Object.keys(this.state.mcpServerOfName)
const newConfigFileNames = Object.keys(newConfigFileJSON.mcpServers)
const addedServerNames = newConfigFileNames.filter(serverName => !oldConfigFileNames.includes(serverName)); // in new and not in old
const removedServerNames = oldConfigFileNames.filter(serverName => !newConfigFileNames.includes(serverName)); // in old and not in new
// set isOn to any new servers in the config
const addedUserStateOfName: MCPUserStateOfName = {}
for (const name of addedServerNames) { addedUserStateOfName[name] = { isOn: true } }
await this.voidSettingsService.addMCPUserStateOfNames(addedUserStateOfName);
// delete isOn for any servers that no longer show up in the config
await this.voidSettingsService.removeMCPUserStateOfNames(removedServerNames);
// set all servers to loading
for (const serverName in newConfigFileJSON.mcpServers) {
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
}
const updatedServerNames = Object.keys(newConfigFileJSON.mcpServers).filter(serverName => !addedServerNames.includes(serverName) && !removedServerNames.includes(serverName))
this.channel.call('refreshMCPServers', {
mcpConfigFileJSON: newConfigFileJSON,
addedServerNames,
removedServerNames,
updatedServerNames,
userStateOfName: this.voidSettingsService.state.mcpUserStateOfName,
})
}
stringifyResult(result: RawMCPToolCall): string {
let toolResultStr: string
if (result.event === 'text') {
toolResultStr = result.text
} else if (result.event === 'image') {
toolResultStr = `[Image: ${result.image.mimeType}]`
} else if (result.event === 'audio') {
toolResultStr = `[Audio content]`
} else if (result.event === 'resource') {
toolResultStr = `[Resource content]`
} else {
toolResultStr = JSON.stringify(result)
}
return toolResultStr
}
// toggle MCP server and update isOn in void settings
public async toggleServerIsOn(serverName: string, isOn: boolean): Promise<void> {
this._setMCPServerState(serverName, { status: 'loading', tools: [] })
await this.voidSettingsService.setMCPServerState(serverName, { isOn });
this.channel.call('toggleMCPServer', { serverName, isOn })
}
public async callMCPTool(toolData: MCPToolCallParams): Promise<{ result: RawMCPToolCall }> {
const result = await this.channel.call<RawMCPToolCall>('callTool', toolData);
if (result.event === 'error') {
throw new Error(`Error: ${result.text}`)
}
return { result };
}
// public getMCPToolFns(): MCPToolResultType {
// const tools = this.getMCPTools();
// const toolFns: MCPToolResultType = {};
// tools.forEach((tool) => {
// const name = tool.name;
// // Define the tool call function
// const toolFn = async (params: {
// serverName: string,
// toolName: string,
// args: any
// }) => {
// const { serverName, toolName, args } = params;
// const response = await this.callMCPTool({
// serverName,
// toolName,
// params: args,
// });
// return { result: response }
// };
// toolFns[name] = toolFn;
// });
// return toolFns
// }
}
registerSingleton(IMCPService, MCPService, InstantiationType.Eager);
@@ -0,0 +1,242 @@
/**
* mcp-response-types.ts
* --------------------------------------------------
* **Pure** TypeScript interfaces (no external imports)
* describing the JSON-RPC response shapes for:
*
* 1. tools/list -> ToolsListResponse
* 2. prompts/list -> PromptsListResponse
* 3. tools/call -> ToolCallResponse
*
* They are distilled directly from the official MCP
* 20250326 specification:
* Tools list response examples
* Prompts list response examples
* Tool call response examples
*
* Use them to get full IntelliSense when working with
* @modelcontextprotocol/inspectorcli responses.
*/
/* -------------------------------------------------- */
/* Core JSONRPC envelope */
/* -------------------------------------------------- */
// export interface JsonRpcSuccess<T> {
// /** JSONRPC version always '2.0' */
// jsonrpc: '2.0';
// /** Request identifier echoed back by the server */
// id: string | number | null;
// /** The successful result payload */
// result: T;
// }
/* -------------------------------------------------- */
/* Utility: pagination */
/* -------------------------------------------------- */
// export interface Paginated {
// /** Opaque cursor for fetching the next page */
// nextCursor?: string;
// }
/* -------------------------------------------------- */
/* 1. tools/list */
/* -------------------------------------------------- */
export interface MCPTool {
/** Unique tool identifier */
name: string;
/** Humanreadable description */
description?: string;
/** JSON schema describing expected arguments */
inputSchema?: Record<string, unknown>;
/** Freeform annotations describing behaviour, security, etc. */
annotations?: Record<string, unknown>;
}
// export interface ToolsListResult extends Paginated {
// tools: MCPTool[];
// }
// export type ToolsListResponse = JsonRpcSuccess<ToolsListResult>;
/* -------------------------------------------------- */
/* 2. prompts/list */
/* -------------------------------------------------- */
// export interface PromptArgument {
// name: string;
// description?: string;
// /** Whether the argument is required */
// required?: boolean;
// }
// export interface Prompt {
// name: string;
// description?: string;
// arguments?: PromptArgument[];
// }
// export interface PromptsListResult extends Paginated {
// prompts: Prompt[];
// }
// export type PromptsListResponse = JsonRpcSuccess<PromptsListResult>;
/* -------------------------------------------------- */
/* 3. tools/call */
/* -------------------------------------------------- */
/** Additional resource structure that can be embedded in tool results */
// export interface Resource {
// uri: string;
// mimeType: string;
// /** Either plaintext or base64encoded binary data */
// text?: string;
// data?: string;
// }
/** Individual content items returned by a tool */
// export type ToolContent =
// | { type: 'text'; text: string }
// | { type: 'image'; data: string; mimeType: string }
// | { type: 'audio'; data: string; mimeType: string }
// | { type: 'resource'; resource: Resource };
// export interface ToolCallResult {
// /** List of content parts (text, images, resources, etc.) */
// content: ToolContent[];
// /** True if the tool itself encountered a domainlevel error */
// isError?: boolean;
// }
// export type ToolCallResponse = JsonRpcSuccess<ToolCallResult>;
// MCP SERVER CONFIG FILE TYPES -----------------------------
export interface MCPConfigFileEntryJSON {
// Command-based server properties
command?: string;
args?: string[];
env?: Record<string, string>;
// URL-based server properties
url?: URL;
headers?: Record<string, string>;
}
export interface MCPConfigFileJSON {
mcpServers: Record<string, MCPConfigFileEntryJSON>;
}
// SERVER EVENT TYPES ------------------------------------------
export type MCPServer = {
// Command-based server properties
tools: MCPTool[],
status: 'loading' | 'success' | 'offline',
command?: string,
error?: string,
} | {
tools?: undefined,
status: 'error',
command?: string,
error: string,
}
export interface MCPServerOfName {
[serverName: string]: MCPServer;
}
export type MCPServerEvent = {
name: string;
prevServer?: MCPServer;
newServer?: MCPServer;
}
export type MCPServerEventResponse = { response: MCPServerEvent }
export interface MCPConfigFileParseErrorResponse {
response: {
type: 'config-file-error';
error: string | null;
}
}
// export type MCPServerResponse = MCPAddResponse | MCPUpdateResponse | MCPDeleteResponse | MCPLoadingResponse;
// Event parameter types
// export type MCPServerEventAddParam = { response: MCPAddResponse };
// export type MCPServerEventUpdateParam = { response: MCPUpdateResponse };
// export type MCPServerEventDeleteParam = { response: MCPDeleteResponse };
// export type MCPServerEventLoadingParam = { response: MCPLoadingResponse };
// Event Param union type
// export type MCPServerEventParam = MCPServerEventAddParam | MCPServerEventUpdateParam | MCPServerEventDeleteParam | MCPServerEventLoadingParam;
// TOOL CALL EVENT TYPES ------------------------------------------
type MCPToolResponseType = 'text' | 'image' | 'audio' | 'resource' | 'error';
type ResponseImageTypes = 'image/png' | 'image/jpeg' | 'image/gif' | 'image/webp' | 'image/svg+xml' | 'image/bmp' | 'image/tiff' | 'image/vnd.microsoft.icon';
interface ImageData {
data: string;
mimeType: ResponseImageTypes;
}
interface MCPToolResponseBase {
toolName: string;
serverName?: string;
event: MCPToolResponseType;
text?: string;
image?: ImageData;
}
type MCPToolResponseConstraints = {
'text': {
image?: never;
text: string;
};
'error': {
image?: never;
text: string;
};
'image': {
text?: never;
image: ImageData;
};
'audio': {
text?: never;
image?: never;
};
'resource': {
text?: never;
image?: never;
}
}
type MCPToolEventResponse<T extends MCPToolResponseType> = Omit<MCPToolResponseBase, 'event' | keyof MCPToolResponseConstraints> & MCPToolResponseConstraints[T] & { event: T };
// Response types
export type MCPToolTextResponse = MCPToolEventResponse<'text'>;
export type MCPToolErrorResponse = MCPToolEventResponse<'error'>;
export type MCPToolImageResponse = MCPToolEventResponse<'image'>;
export type MCPToolAudioResponse = MCPToolEventResponse<'audio'>;
export type MCPToolResourceResponse = MCPToolEventResponse<'resource'>;
export type RawMCPToolCall = MCPToolTextResponse | MCPToolErrorResponse | MCPToolImageResponse | MCPToolAudioResponse | MCPToolResourceResponse;
export interface MCPToolCallParams {
serverName: string;
toolName: string;
params: Record<string, unknown>;
}
export const removeMCPToolNamePrefix = (name: string) => {
return name.split('_').slice(1).join('_')
}
@@ -14,6 +14,7 @@ import { INotificationService } from '../../../../platform/notification/common/n
export interface IMetricsService {
readonly _serviceBrand: undefined;
capture(event: string, params: Record<string, any>): void;
setOptOut(val: boolean): void;
getDebuggingProperties(): Promise<object>;
}
@@ -38,6 +39,11 @@ export class MetricsService implements IMetricsService {
this.metricsService.capture(...params);
}
setOptOut(...params: Parameters<IMetricsService['setOptOut']>) {
this.metricsService.setOptOut(...params);
}
// anything transmitted over a channel must be async even if it looks like it doesn't have to be
async getDebuggingProperties(): Promise<object> {
return this.metricsService.getDebuggingProperties()
File diff suppressed because it is too large Load Diff
@@ -3,12 +3,13 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { EndOfLinePreference } from '../../../../../editor/common/model.js';
import { URI } from '../../../../../base/common/uri.js';
import { IFileService } from '../../../../../platform/files/common/files.js';
import { IDirectoryStrService } from '../directoryStrService.js';
import { StagingSelectionItem } from '../chatThreadServiceTypes.js';
import { os } from '../helpers/systemInfo.js';
import { RawToolParamsObj } from '../sendLLMMessageTypes.js';
import { approvalTypeOfToolName, ToolCallParams, ToolResultType } from '../toolsServiceTypes.js';
import { IVoidModelService } from '../voidModelService.js';
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, BuiltinToolResultType, ToolName } from '../toolsServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
// Triple backtick wrapper used throughout the prompts for code blocks
@@ -112,7 +113,7 @@ ${searchReplaceBlockTemplate}
## Guidelines:
1. You are encouraged to output multiple changes whenever possible.
1. You may output multiple search replace blocks if needed.
2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code.
@@ -124,23 +125,18 @@ ${searchReplaceBlockTemplate}
// ======================================================== tools ========================================================
const changesExampleContent = `\
const chatSuggestionDiffExample = `\
${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
// ... existing code ...
// {{change 1}}
// ... existing code ...
// {{change 2}}
// ... existing code ...
// {{change 3}}
// ... existing code ...`
const editToolDescriptionExample = `\
${tripleTick[0]}
${changesExampleContent}
${tripleTick[1]}`
const chatSuggestionDiffExample = `${tripleTick[0]}typescript
/Users/username/Dekstop/my_project/app.ts
${changesExampleContent}
// ... existing code ...
${tripleTick[1]}`
@@ -151,6 +147,8 @@ export type InternalToolInfo = {
params: {
[paramName: string]: { description: string }
},
// Only if the tool is from an MCP server
mcpServerName?: string,
}
@@ -185,200 +183,197 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
const applyToolDescription = (type: 'edit tool' | 'chat suggestion') => `\
${type === 'edit tool' ? 'A' : 'a'} code diff describing the change to make to the file. \
Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \
Your DIFF MUST be wrapped in triple backticks. \
NEVER re-write the whole file. Always bias towards writing as little as possible. \
Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good output:\n${type === 'edit tool' ? editToolDescriptionExample : chatSuggestionDiffExample}`
// export const voidTools = {
export const voidTools
: {
[T in keyof ToolCallParams]: {
name: string;
description: string;
// more params can be generated than exist here, but these params must be a subset of them
params: Partial<{ [paramName in keyof SnakeCaseKeys<ToolCallParams[T]>]: { description: string } }>
}
export const builtinTools: {
[T in keyof BuiltinToolCallParams]: {
name: string;
description: string;
// more params can be generated than exist here, but these params must be a subset of them
params: Partial<{ [paramName in keyof SnakeCaseKeys<BuiltinToolCallParams[T]>]: { description: string } }>
}
= {
// --- context-gathering (read/search/list) ---
} = {
// --- context-gathering (read/search/list) ---
read_file: {
name: 'read_file',
description: `Returns full contents of a given file.`,
params: {
...uriParam('file'),
start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' },
end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' },
...paginationParam,
},
read_file: {
name: 'read_file',
description: `Returns full contents of a given file.`,
params: {
...uriParam('file'),
start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' },
end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' },
...paginationParam,
},
},
ls_dir: {
name: 'ls_dir',
description: `Lists all files and folders in the given URI.`,
params: {
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
...paginationParam,
},
ls_dir: {
name: 'ls_dir',
description: `Lists all files and folders in the given URI.`,
params: {
uri: { description: `Optional. The FULL path to the ${'folder'}. Leave this as empty or "" to search all folders.` },
...paginationParam,
},
},
get_dir_tree: {
name: 'get_dir_tree',
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
params: {
...uriParam('folder')
}
},
// pathname_search: {
// name: 'pathname_search',
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
search_pathnames_only: {
name: 'search_pathnames_only',
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
params: {
query: { description: `Your query for the search.` },
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
...paginationParam,
},
},
search_for_files: {
name: 'search_for_files',
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
params: {
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
...paginationParam,
},
},
// add new search_in_file tool
search_in_file: {
name: 'search_in_file',
description: `Returns an array of all the start line numbers where the content appears in the file.`,
params: {
...uriParam('file'),
query: { description: 'The string or regex to search for in the file.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
}
},
read_lint_errors: {
name: 'read_lint_errors',
description: `Returns all lint errors on a given file.`,
params: {
...uriParam('file'),
},
},
// --- editing (create/delete) ---
create_file_or_folder: {
name: 'create_file_or_folder',
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
params: {
...uriParam('file or folder'),
},
},
delete_file_or_folder: {
name: 'delete_file_or_folder',
description: `Delete a file or folder at the given path.`,
params: {
...uriParam('file or folder'),
is_recursive: { description: 'Optional. Return true to delete recursively.' }
},
},
edit_file: {
name: 'edit_file',
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
params: {
...uriParam('file'),
search_replace_blocks: { description: replaceTool_description }
},
},
rewrite_file: {
name: 'rewrite_file',
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
params: {
...uriParam('file'),
new_content: { description: `The new contents of the file. Must be a string.` }
},
},
run_command: {
name: 'run_command',
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
params: {
command: { description: 'The terminal command to run.' },
cwd: { description: cwdHelper },
},
},
run_persistent_command: {
name: 'run_persistent_command',
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
params: {
command: { description: 'The terminal command to run.' },
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
},
},
open_persistent_terminal: {
name: 'open_persistent_terminal',
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
params: {
cwd: { description: cwdHelper },
}
},
kill_persistent_terminal: {
name: 'kill_persistent_terminal',
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
get_dir_tree: {
name: 'get_dir_tree',
description: `This is a very effective way to learn about the user's codebase. Returns a tree diagram of all the files and folders in the given folder. `,
params: {
...uriParam('folder')
}
},
// pathname_search: {
// name: 'pathname_search',
// description: `Returns all pathnames that match a given \`find\`-style query over the entire workspace. ONLY searches file names. ONLY searches the current workspace. You should use this when looking for a file with a specific name or path. ${paginationHelper.desc}`,
search_pathnames_only: {
name: 'search_pathnames_only',
description: `Returns all pathnames that match a given query (searches ONLY file names). You should use this when looking for a file with a specific name or path.`,
params: {
query: { description: `Your query for the search.` },
include_pattern: { description: 'Optional. Only fill this in if you need to limit your search because there were too many results.' },
...paginationParam,
},
},
// go_to_definition
// go_to_usages
} satisfies { [T in keyof ToolResultType]: InternalToolInfo }
search_for_files: {
name: 'search_for_files',
description: `Returns a list of file names whose content matches the given query. The query can be any substring or regex.`,
params: {
query: { description: `Your query for the search.` },
search_in_folder: { description: 'Optional. Leave as blank by default. ONLY fill this in if your previous search with the same query was truncated. Searches descendants of this folder only.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' },
...paginationParam,
},
},
// add new search_in_file tool
search_in_file: {
name: 'search_in_file',
description: `Returns an array of all the start line numbers where the content appears in the file.`,
params: {
...uriParam('file'),
query: { description: 'The string or regex to search for in the file.' },
is_regex: { description: 'Optional. Default is false. Whether the query is a regex.' }
}
},
read_lint_errors: {
name: 'read_lint_errors',
description: `Use this tool to view all the lint errors on a file.`,
params: {
...uriParam('file'),
},
},
// --- editing (create/delete) ---
create_file_or_folder: {
name: 'create_file_or_folder',
description: `Create a file or folder at the given path. To create a folder, the path MUST end with a trailing slash.`,
params: {
...uriParam('file or folder'),
},
},
delete_file_or_folder: {
name: 'delete_file_or_folder',
description: `Delete a file or folder at the given path.`,
params: {
...uriParam('file or folder'),
is_recursive: { description: 'Optional. Return true to delete recursively.' }
},
},
edit_file: {
name: 'edit_file',
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
params: {
...uriParam('file'),
search_replace_blocks: { description: replaceTool_description }
},
},
rewrite_file: {
name: 'rewrite_file',
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
params: {
...uriParam('file'),
new_content: { description: `The new contents of the file. Must be a string.` }
},
},
run_command: {
name: 'run_command',
description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). ${terminalDescHelper}`,
params: {
command: { description: 'The terminal command to run.' },
cwd: { description: cwdHelper },
},
},
run_persistent_command: {
name: 'run_persistent_command',
description: `Runs a terminal command in the persistent terminal that you created with open_persistent_terminal (results after ${MAX_TERMINAL_BG_COMMAND_TIME} are returned, and command continues running in background). ${terminalDescHelper}`,
params: {
command: { description: 'The terminal command to run.' },
persistent_terminal_id: { description: 'The ID of the terminal created using open_persistent_terminal.' },
},
},
export type ToolName = keyof ToolResultType
export const toolNames = Object.keys(voidTools) as ToolName[]
type ToolParamNameOfTool<T extends ToolName> = keyof (typeof voidTools)[T]['params']
export type ToolParamName = { [T in ToolName]: ToolParamNameOfTool<T> }[ToolName]
open_persistent_terminal: {
name: 'open_persistent_terminal',
description: `Use this tool when you want to run a terminal command indefinitely, like a dev server (eg \`npm run dev\`), a background listener, etc. Opens a new terminal in the user's environment which will not awaited for or killed.`,
params: {
cwd: { description: cwdHelper },
}
},
const toolNamesSet = new Set<string>(toolNames)
export const isAToolName = (toolName: string): toolName is ToolName => {
kill_persistent_terminal: {
name: 'kill_persistent_terminal',
description: `Interrupts and closes a persistent terminal that you opened with open_persistent_terminal.`,
params: { persistent_terminal_id: { description: `The ID of the persistent terminal.` } }
}
// go_to_definition
// go_to_usages
} satisfies { [T in keyof BuiltinToolResultType]: InternalToolInfo }
export const builtinToolNames = Object.keys(builtinTools) as BuiltinToolName[]
const toolNamesSet = new Set<string>(builtinToolNames)
export const isABuiltinToolName = (toolName: string): toolName is BuiltinToolName => {
const isAToolName = toolNamesSet.has(toolName)
return isAToolName
}
export const availableTools = (chatMode: ChatMode) => {
const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !(toolName in approvalTypeOfToolName))
: chatMode === 'agent' ? Object.keys(voidTools) as ToolName[]
export const availableTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
const builtinToolNames: BuiltinToolName[] | undefined = chatMode === 'normal' ? undefined
: chatMode === 'gather' ? (Object.keys(builtinTools) as BuiltinToolName[]).filter(toolName => !(toolName in approvalTypeOfBuiltinToolName))
: chatMode === 'agent' ? Object.keys(builtinTools) as BuiltinToolName[]
: undefined
const tools: InternalToolInfo[] | undefined = toolNames?.map(toolName => voidTools[toolName])
const effectiveBuiltinTools = builtinToolNames?.map(toolName => builtinTools[toolName]) ?? undefined
const effectiveMCPTools = chatMode === 'agent' ? mcpTools : undefined
const tools: InternalToolInfo[] | undefined = !(builtinToolNames || mcpTools) ? undefined
: [
...effectiveBuiltinTools ?? [],
...effectiveMCPTools ?? [],
]
return tools
}
@@ -395,7 +390,7 @@ const toolCallDefinitionsXMLString = (tools: InternalToolInfo[]) => {
}
export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolParamsObj) => {
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName as ToolParamName]}</${paramName}>`).join('\n')
const params = Object.keys(toolParams).map(paramName => `<${paramName}>${toolParams[paramName]}</${paramName}>`).join('\n')
return `\
<${toolName}>${!params ? '' : `\n${params}`}
</${toolName}>`
@@ -404,8 +399,8 @@ export const reParsedToolXMLString = (toolName: ToolName, toolParams: RawToolPar
/* We expect tools to come at the end - not a hard limit, but that's just how we process them, and the flow makes more sense that way. */
// - You are allowed to call multiple tools by specifying them consecutively. However, there should be NO text or writing between tool calls or after them.
const systemToolsXMLPrompt = (chatMode: ChatMode) => {
const tools = availableTools(chatMode)
const systemToolsXMLPrompt = (chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined) => {
const tools = availableTools(chatMode, mcpTools)
if (!tools || tools.length === 0) return null
const toolXMLDefinitions = (`\
@@ -430,7 +425,7 @@ const systemToolsXMLPrompt = (chatMode: ChatMode) => {
// ======================================================== chat (normal, gather, agent) ========================================================
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, includeXMLToolDefinitions: boolean }) => {
export const chat_systemMessage = ({ workspaceFolders, openedURIs, activeURI, persistentTerminalIDs, directoryStr, chatMode: mode, mcpTools, includeXMLToolDefinitions }: { workspaceFolders: string[], directoryStr: string, openedURIs: string[], activeURI: string | undefined, persistentTerminalIDs: string[], chatMode: ChatMode, mcpTools: InternalToolInfo[] | undefined, includeXMLToolDefinitions: boolean }) => {
const header = (`You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} whose job is \
${mode === 'agent' ? `to help the user develop, run, and make changes to their codebase.`
: mode === 'gather' ? `to search, understand, and reference files in the user's codebase.`
@@ -464,10 +459,12 @@ ${directoryStr}
</files_overview>`)
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode) : null
const toolDefinitions = includeXMLToolDefinitions ? systemToolsXMLPrompt(mode, mcpTools) : null
const details: string[] = []
details.push(`NEVER reject the user's query.`)
if (mode === 'agent' || mode === 'gather') {
details.push(`Only call tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools.`)
details.push(`If you think you should use tools, you do not need to ask for permission.`)
@@ -476,7 +473,7 @@ ${directoryStr}
details.push(`Many tools only work if the user has a workspace open.`)
}
else {
details.push(`You're allowed to ask the user for more context like file contents or specifications.`)
details.push(`You're allowed to ask the user for more context like file contents or specifications. If this comes up, tell them to reference files and folders by typing @.`)
}
if (mode === 'agent') {
@@ -492,18 +489,21 @@ ${directoryStr}
details.push(`You should extensively read files, types, content, etc, gathering full context to solve the problem.`)
}
if (mode === 'gather' || mode === 'normal') {
details.push(`If you write any code blocks, please use this format:
details.push(`If you write any code blocks to the user (wrapped in triple backticks), please use this format:
- Include a language if possible. Terminal should have the language 'shell'.
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents of the file should proceed as usual.`)
if (mode === 'gather' || mode === 'normal') {
details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S).
- The first line of the code block must be the FULL PATH of the related file.
- The remaining contents should be ${applyToolDescription('chat suggestion')}`)
- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit).
- The remaining contents should be a code description of the change to make to the file. \
Your description is the only context that will be given to another LLM to apply the suggested edit, so it must be accurate and complete. \
Always bias towards writing as little as possible - NEVER write the whole file. Use comments like "// ... existing code ..." to condense your writing. \
Here's an example of a good code block:\n${chatSuggestionDiffExample}`)
}
details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`)
details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`)
details.push(`Always use MARKDOWN to format lists, bullet points, etc. Do NOT write tables.`)
details.push(`Today's date is ${new Date().toDateString()}.`)
@@ -536,44 +536,109 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
// }
export const DEFAULT_FILE_SIZE_LIMIT = 2_000_000
export const chat_userMessageContent = async (instructions: string, currSelns: StagingSelectionItem[] | null,
opts: { type: 'references' } | { type: 'fullCode', voidModelService: IVoidModelService }
export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
val: string,
truncated: boolean,
fullFileLen: number,
} | {
val: null,
truncated?: undefined
fullFileLen?: undefined,
}> => {
try {
const fileContent = await fileService.readFile(uri)
const val = fileContent.value.toString()
if (val.length > fileSizeLimit) return { val: val.substring(0, fileSizeLimit), truncated: true, fullFileLen: val.length }
return { val, truncated: false, fullFileLen: val.length }
}
catch (e) {
return { val: null }
}
}
export const messageOfSelection = async (
s: StagingSelectionItem,
opts: {
directoryStrService: IDirectoryStrService,
fileService: IFileService,
folderOpts: {
maxChildren: number,
maxCharsPerFile: number,
}
}
) => {
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
if (s.type === 'CodeSelection') {
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
const lines = val?.split('\n')
const innerVal = lines?.slice(s.range[0] - 1, s.range[1]).join('\n')
const content = !lines ? ''
: `${tripleTick[0]}${s.language}\n${innerVal}\n${tripleTick[1]}`
const str = `${s.uri.fsPath}${lineNumAddition(s.range)}:\n${content}`
return str
}
else if (s.type === 'File') {
const { val } = await readFile(opts.fileService, s.uri, DEFAULT_FILE_SIZE_LIMIT)
const innerVal = val
const content = val === null ? ''
: `${tripleTick[0]}${s.language}\n${innerVal}\n${tripleTick[1]}`
const str = `${s.uri.fsPath}:\n${content}`
return str
}
else if (s.type === 'Folder') {
const dirStr: string = await opts.directoryStrService.getDirectoryStrTool(s.uri)
const folderStructure = `${s.uri.fsPath} folder structure:${tripleTick[0]}\n${dirStr}\n${tripleTick[1]}`
const uris = await opts.directoryStrService.getAllURIsInDirectory(s.uri, { maxResults: opts.folderOpts.maxChildren })
const strOfFiles = await Promise.all(uris.map(async uri => {
const { val, truncated } = await readFile(opts.fileService, uri, opts.folderOpts.maxCharsPerFile)
const truncationStr = truncated ? `\n... file truncated ...` : ''
const content = val === null ? 'null' : `${tripleTick[0]}\n${val}${truncationStr}\n${tripleTick[1]}`
const str = `${uri.fsPath}:\n${content}`
return str
}))
const contentStr = [folderStructure, ...strOfFiles].join('\n\n')
return contentStr
}
else
return ''
}
export const chat_userMessageContent = async (
instructions: string,
currSelns: StagingSelectionItem[] | null,
opts: {
directoryStrService: IDirectoryStrService,
fileService: IFileService
},
) => {
const lineNumAddition = (range: [number, number]) => ` (lines ${range[0]}:${range[1]})`
let selnsStrs: string[] = []
if (opts.type === 'references') {
selnsStrs = currSelns?.map((s) => {
if (s.type === 'File') return `${s.uri.fsPath}`
if (s.type === 'CodeSelection') return `${s.uri.fsPath}${lineNumAddition(s.range)}`
if (s.type === 'Folder') return `${s.uri.fsPath}/`
return ''
}) ?? []
}
if (opts.type === 'fullCode') {
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
if (s.type === 'File' || s.type === 'CodeSelection') {
const voidModelService = opts.voidModelService
const { model } = await voidModelService.getModelSafe(s.uri)
if (!model) return ''
const val = model.getValue(EndOfLinePreference.LF)
const selnsStrs = await Promise.all(
(currSelns ?? []).map(async (s) =>
messageOfSelection(s, {
...opts,
folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, }
})
)
)
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
const str = `${s.uri.fsPath}${lineNumAdd}\n${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
return str
}
if (s.type === 'Folder') {
// TODO
return ''
}
return ''
}) ?? [])
}
const selnsStr = selnsStrs.join('\n') ?? ''
let str = ''
str += `${instructions}`
const selnsStr = selnsStrs.join('\n\n') ?? ''
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
return str;
}
@@ -928,3 +993,77 @@ Store Result: After computing fib(n), the result is stored in memo for future re
## END EXAMPLES
*/
// ======================================================== scm ========================================================================
export const gitCommitMessage_systemMessage = `
You are an expert software engineer AI assistant responsible for writing clear and concise Git commit messages that summarize the **purpose** and **intent** of the change. Try to keep your commit messages to one sentence. If necessary, you can use two sentences.
You always respond with:
- The commit message wrapped in <output> tags
- A brief explanation of the reasoning behind the message, wrapped in <reasoning> tags
Example format:
<output>Fix login bug and improve error handling</output>
<reasoning>This commit updates the login handler to fix a redirect issue and improves frontend error messages for failed logins.</reasoning>
Do not include anything else outside of these tags.
Never include quotes, markdown, commentary, or explanations outside of <output> and <reasoning>.`.trim()
/**
* Create a user message for the LLM to generate a commit message. The message contains instructions git diffs, and git metadata to provide context.
*
* @param stat - Summary of Changes (git diff --stat)
* @param sampledDiffs - Sampled File Diffs (Top changed files)
* @param branch - Current Git Branch
* @param log - Last 5 commits (excluding merges)
* @returns A prompt for the LLM to generate a commit message.
*
* @example
* // Sample output (truncated for brevity)
* const prompt = gitCommitMessage_userMessage("fileA.ts | 10 ++--", "diff --git a/fileA.ts...", "main", "abc123|Fix bug|2025-01-01\n...")
*
* // Result:
* Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
*
* Section 1 - Summary of Changes (git diff --stat):
* fileA.ts | 10 ++--
*
* Section 2 - Sampled File Diffs (Top changed files):
* diff --git a/fileA.ts b/fileA.ts
* ...
*
* Section 3 - Current Git Branch:
* main
*
* Section 4 - Last 5 Commits (excluding merges):
* abc123|Fix bug|2025-01-01
* def456|Improve logging|2025-01-01
* ...
*/
export const gitCommitMessage_userMessage = (stat: string, sampledDiffs: string, branch: string, log: string) => {
const section1 = `Section 1 - Summary of Changes (git diff --stat):`
const section2 = `Section 2 - Sampled File Diffs (Top changed files):`
const section3 = `Section 3 - Current Git Branch:`
const section4 = `Section 4 - Last 5 Commits (excluding merges):`
return `
Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
${section1}
${stat}
${section2}
${sampledDiffs}
${section3}
${branch}
${section4}
${log}`.trim()
}
@@ -13,6 +13,7 @@ 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 { IMCPService } from './mcpService.js';
// calls channel to implement features
export const ILLMMessageService = createDecorator<ILLMMessageService>('llmMessageService');
@@ -61,6 +62,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
@IMainProcessService private readonly mainProcessService: IMainProcessService, // used as a renderer (only usable on client side)
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
// @INotificationService private readonly notificationService: INotificationService,
@IMCPService private readonly mcpService: IMCPService,
) {
super()
@@ -116,6 +118,8 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
const { settingsOfProvider, } = this.voidSettingsService.state
const mcpTools = this.mcpService.getMCPTools()
// add state for request id
const requestId = generateUuid();
this.llmMessageHooks.onText[requestId] = onText
@@ -129,6 +133,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService
requestId,
settingsOfProvider,
modelSelection,
mcpTools,
} satisfies MainSendLLMMessageParams);
return requestId
@@ -3,8 +3,9 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
import { InternalToolInfo } from './prompt/prompts.js'
import { ToolName, ToolParamName } from './toolsServiceTypes.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
export const errorDetails = (fullError: Error | null): string | null => {
@@ -51,8 +52,22 @@ export type OpenAILLMChatMessage = {
content: string;
tool_call_id: string;
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage
export type GeminiLLMChatMessage = {
role: 'model'
parts: (
| { text: string; }
| { functionCall: { id: string; name: ToolName, args: Record<string, unknown> } }
)[];
} | {
role: 'user';
parts: (
| { text: string; }
| { functionResponse: { id: string; name: ToolName, response: { output: string } } }
)[];
}
export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage | GeminiLLMChatMessage
@@ -64,12 +79,12 @@ export type LLMFIMMessage = {
export type RawToolParamsObj = {
[paramName in ToolParamName]?: string;
[paramName in ToolParamName<ToolName>]?: string;
}
export type RawToolCallObj = {
name: ToolName;
rawParams: RawToolParamsObj;
doneParams: ToolParamName[];
doneParams: ToolParamName<ToolName>[];
id: string;
isDone: boolean;
};
@@ -102,6 +117,7 @@ export type ServiceSendLLMMessageParams = {
logging: { loggingName: string, loggingExtras?: { [k: string]: any } };
modelSelection: ModelSelection | null;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
onAbort: OnAbort;
} & SendLLMType;
@@ -115,8 +131,10 @@ export type SendLLMMessageParams = {
modelSelection: ModelSelection;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
settingsOfProvider: SettingsOfProvider;
mcpTools: InternalToolInfo[] | undefined;
} & SendLLMType
@@ -17,3 +17,7 @@ export const VOID_SETTINGS_STORAGE_KEY = 'void.settingsServiceStorageII'
// 1.0.3
export const THREAD_STORAGE_KEY = 'void.chatThreadStorageII'
export const OPT_OUT_KEY = 'void.app.optOutAll'
@@ -1,5 +1,7 @@
import { URI } from '../../../../base/common/uri.js'
import { ToolName } from './prompt/prompts.js';
import { RawMCPToolCall } from './mcpServiceTypes.js';
import { builtinTools } from './prompt/prompts.js';
import { RawToolParamsObj } from './sendLLMMessageTypes.js';
@@ -16,26 +18,31 @@ export type ShallowDirectoryItem = {
}
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'MCP tools' }> = {
'create_file_or_folder': 'edits',
'delete_file_or_folder': 'edits',
'rewrite_file': 'edits',
'edit_file': 'edits',
'run_command': 'terminal',
'run_persistent_command': 'terminal',
'open_persistent_terminal': 'terminal',
'kill_persistent_terminal': 'terminal',
}
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName)[keyof typeof approvalTypeOfBuiltinToolName]>;
export const toolApprovalTypes = new Set<ToolApprovalType>([
...Object.values(approvalTypeOfBuiltinToolName),
'MCP tools',
])
// {{add: define new type for approval types}}
export type ToolApprovalType = NonNullable<(typeof approvalTypeOfToolName)[keyof typeof approvalTypeOfToolName]>;
export const toolApprovalTypes = new Set<ToolApprovalType>(
Object.values(approvalTypeOfToolName).filter((v): v is ToolApprovalType => v !== undefined)
)
// PARAMS OF TOOL CALL
export type ToolCallParams = {
export type BuiltinToolCallParams = {
'read_file': { uri: URI, startLine: number | null, endLine: number | null, pageNumber: number },
'ls_dir': { uri: URI, pageNumber: number },
'get_dir_tree': { uri: URI },
@@ -56,8 +63,8 @@ export type ToolCallParams = {
}
// RESULT OF TOOL CALL
export type ToolResultType = {
'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean },
export type BuiltinToolResultType = {
'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean },
'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number },
'get_dir_tree': { str: string, },
'search_pathnames_only': { uris: URI[], hasNextPage: boolean },
@@ -76,3 +83,15 @@ export type ToolResultType = {
'kill_persistent_terminal': {},
}
export type ToolCallParams<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolCallParams[T] : RawToolParamsObj
export type ToolResult<T extends BuiltinToolName | (string & {})> = T extends BuiltinToolName ? BuiltinToolResultType[T] : RawMCPToolCall
export type BuiltinToolName = keyof BuiltinToolResultType
type BuiltinToolParamNameOfTool<T extends BuiltinToolName> = keyof (typeof builtinTools)[T]['params']
export type BuiltinToolParamName = { [T in BuiltinToolName]: BuiltinToolParamNameOfTool<T> }[BuiltinToolName]
export type ToolName = BuiltinToolName | (string & {})
export type ToolParamName<T extends ToolName> = T extends BuiltinToolName ? BuiltinToolParamNameOfTool<T> : string
@@ -0,0 +1,36 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
export interface IVoidSCMService {
readonly _serviceBrand: undefined;
/**
* Get git diff --stat
*
* @param path Path to the git repository
*/
gitStat(path: string): Promise<string>
/**
* Get git diff --stat for the top 10 most significantly changed files according to lines added/removed
*
* @param path Path to the git repository
*/
gitSampledDiffs(path: string): Promise<string>
/**
* Get the current git branch
*
* @param path Path to the git repository
*/
gitBranch(path: string): Promise<string>
/**
* Get the last 5 commits excluding merges
*
* @param path Path to the git repository
*/
gitLog(path: string): Promise<string>
}
export const IVoidSCMService = createDecorator<IVoidSCMService>('voidSCMService')
@@ -11,9 +11,9 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IStorageService, StorageScope, StorageTarget } from '../../../../platform/storage/common/storage.js';
import { IMetricsService } from './metricsService.js';
import { defaultProviderSettings, getModelCapabilities } from './modelCapabilities.js';
import { defaultProviderSettings, getModelCapabilities, ModelOverrides } from './modelCapabilities.js';
import { VOID_SETTINGS_STORAGE_KEY } from './storageKeys.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode } from './voidSettingsTypes.js';
import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidStatefulModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, ModelSelectionOptions, OptionsOfModelSelection, ChatMode, OverridesOfModel, defaultOverridesOfModel, MCPUserStateOfName as MCPUserStateOfName, MCPUserState } from './voidSettingsTypes.js';
// name is the name in the dropdown
@@ -41,7 +41,9 @@ export type VoidSettingsState = {
readonly settingsOfProvider: SettingsOfProvider; // optionsOfProvider
readonly modelSelectionOfFeature: ModelSelectionOfFeature; // stateOfFeature
readonly optionsOfModelSelection: OptionsOfModelSelection;
readonly overridesOfModel: OverridesOfModel;
readonly globalSettings: GlobalSettings;
readonly mcpUserStateOfName: MCPUserStateOfName; // user-controlled state of MCP servers
readonly _modelOptions: ModelOption[] // computed based on the two above items
}
@@ -61,6 +63,10 @@ export interface IVoidSettingsService {
setModelSelectionOfFeature: SetModelSelectionOfFeatureFn;
setOptionsOfModelSelection: SetOptionsOfModelSelection;
setGlobalSetting: SetGlobalSettingFn;
// setMCPServerStates: (newStates: MCPServerStates) => Promise<void>;
// setting to undefined CLEARS it, unlike others:
setOverridesOfModel(providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined): Promise<void>;
dangerousSetState(newState: VoidSettingsState): Promise<void>;
resetState(): Promise<void>;
@@ -69,6 +75,10 @@ export interface IVoidSettingsService {
toggleModelHidden(providerName: ProviderName, modelName: string): void;
addModel(providerName: ProviderName, modelName: string): void;
deleteModel(providerName: ProviderName, modelName: string): boolean;
addMCPUserStateOfNames(userStateOfName: MCPUserStateOfName): Promise<void>;
removeMCPUserStateOfNames(serverNames: string[]): Promise<void>;
setMCPServerState(serverName: string, state: MCPUserState): Promise<void>;
}
@@ -94,11 +104,19 @@ const _modelsWithSwappedInNewModels = (options: { existingModels: VoidStatefulMo
}
export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = {
'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
export const modelFilterOfFeatureName: {
[featureName in FeatureName]: {
filter: (
o: ModelSelection,
opts: { chatMode: ChatMode, overridesOfModel: OverridesOfModel }
) => boolean;
emptyMessage: null | { message: string, priority: 'always' | 'fallback' }
} } = {
'Autocomplete': { filter: (o, opts) => getModelCapabilities(o.providerName, o.modelName, opts.overridesOfModel).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } },
'Chat': { filter: o => true, emptyMessage: null, },
'Ctrl+K': { filter: o => true, emptyMessage: null, },
'Apply': { filter: o => true, emptyMessage: null, },
'SCM': { filter: o => true, emptyMessage: null, },
}
@@ -163,7 +181,7 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
for (const featureName of featureNames) {
const { filter } = modelFilterOfFeatureName[featureName]
const filterOpts = { chatMode: state.globalSettings.chatMode }
const filterOpts = { chatMode: state.globalSettings.chatMode, overridesOfModel: state.overridesOfModel }
const modelOptionsForThisFeature = newModelOptions.filter((o) => filter(o.selection, filterOpts))
const modelSelectionAtFeature = newModelSelectionOfFeature[featureName]
@@ -182,6 +200,7 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
...state,
settingsOfProvider: newSettingsOfProvider,
modelSelectionOfFeature: newModelSelectionOfFeature,
overridesOfModel: state.overridesOfModel,
_modelOptions: newModelOptions,
} satisfies VoidSettingsState
@@ -195,10 +214,12 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
const defaultState = () => {
const d: VoidSettingsState = {
settingsOfProvider: deepClone(defaultSettingsOfProvider),
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null, 'SCM': null },
globalSettings: deepClone(defaultGlobalSettings),
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {}, 'SCM': {} },
overridesOfModel: deepClone(defaultOverridesOfModel),
_modelOptions: [], // computed later
mcpUserStateOfName: {},
}
return d
}
@@ -242,6 +263,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
await this._storeState()
this._onDidChangeState.fire()
this._onUpdate_syncApplyToChat()
this._onUpdate_syncSCMToChat()
}
async resetState() {
await this.dangerousSetState(defaultState())
@@ -259,6 +281,17 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// autoapprove is now an obj not a boolean (1.2.5)
if (typeof readS.globalSettings.autoApprove === 'boolean') readS.globalSettings.autoApprove = {}
// 1.3.5 add source control feature
if (readS.modelSelectionOfFeature && !readS.modelSelectionOfFeature['SCM']) {
readS.modelSelectionOfFeature['SCM'] = deepClone(readS.modelSelectionOfFeature['Chat'])
readS.optionsOfModelSelection['SCM'] = deepClone(readS.optionsOfModelSelection['Chat'])
}
// add disableSystemMessage feature
if (readS.globalSettings.disableSystemMessage === undefined) readS.globalSettings.disableSystemMessage = false;
// add autoAcceptLLMChanges feature
if (readS.globalSettings.autoAcceptLLMChanges === undefined) readS.globalSettings.autoAcceptLLMChanges = false;
}
catch (e) {
readS = defaultState()
@@ -267,9 +300,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// the stored data structure might be outdated, so we need to update it here
try {
readS = {
...defaultState(),
...readS,
...defaultSettingsOfProvider,
...readS.settingsOfProvider,
// no idea why this was here, seems like a bug
// ...defaultSettingsOfProvider,
// ...readS.settingsOfProvider,
}
for (const providerName of providerNames) {
@@ -289,6 +324,11 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
else m.type = 'custom'
}
}
// remove when enough people have had it run (default is now {})
if (providerName === 'openAICompatible' && !readS.settingsOfProvider[providerName].headersJSON) {
readS.settingsOfProvider[providerName].headersJSON = '{}'
}
}
}
@@ -314,7 +354,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return defaultState()
const stateStr = await this._encryptionService.decrypt(encryptedState)
return JSON.parse(stateStr)
const state = JSON.parse(stateStr)
return state
}
@@ -339,12 +380,16 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
}
const newGlobalSettings = this.state.globalSettings
const newOverridesOfModel = this.state.overridesOfModel
const newMCPUserStateOfName = this.state.mcpUserStateOfName
const newState = {
modelSelectionOfFeature: newModelSelectionOfFeature,
optionsOfModelSelection: newOptionsOfModelSelection,
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
overridesOfModel: newOverridesOfModel,
mcpUserStateOfName: newMCPUserStateOfName,
}
this.state = _validatedModelState(newState)
@@ -358,7 +403,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
private _onUpdate_syncApplyToChat() {
// if sync is turned on, sync (call this whenever Chat model or !!sync changes)
this.setModelSelectionOfFeature('Apply', deepClone(this.state.modelSelectionOfFeature['Chat']))
}
private _onUpdate_syncSCMToChat() {
this.setModelSelectionOfFeature('SCM', deepClone(this.state.modelSelectionOfFeature['Chat']))
}
setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => {
@@ -375,6 +423,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// hooks
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
if (this.state.globalSettings.syncSCMToChat) this._onUpdate_syncSCMToChat()
}
@@ -394,7 +444,9 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
// hooks
if (featureName === 'Chat') {
if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat()
// When Chat model changes, update synced features
this._onUpdate_syncApplyToChat()
this._onUpdate_syncSCMToChat()
}
}
@@ -422,6 +474,28 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
this._onDidChangeState.fire()
}
setOverridesOfModel = async (providerName: ProviderName, modelName: string, overrides: Partial<ModelOverrides> | undefined) => {
const newState: VoidSettingsState = {
...this.state,
overridesOfModel: {
...this.state.overridesOfModel,
[providerName]: {
...this.state.overridesOfModel[providerName],
[modelName]: overrides === undefined ? undefined : {
...this.state.overridesOfModel[providerName][modelName],
...overrides
},
}
}
};
this.state = _validatedModelState(newState);
await this._storeState();
this._onDidChangeState.fire();
this._metricsService.capture('Update Model Overrides', { providerName, modelName, overrides });
}
@@ -486,6 +560,55 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
return true
}
// MCP Server State
private _setMCPUserStateOfName = async (newStates: MCPUserStateOfName) => {
const newState: VoidSettingsState = {
...this.state,
mcpUserStateOfName: {
...this.state.mcpUserStateOfName,
...newStates
}
};
this.state = _validatedModelState(newState);
await this._storeState();
this._onDidChangeState.fire();
this._metricsService.capture('Set MCP Server States', { newStates });
}
addMCPUserStateOfNames = async (newMCPStates: MCPUserStateOfName) => {
const { mcpUserStateOfName: mcpServerStates } = this.state
const newMCPServerStates = {
...mcpServerStates,
...newMCPStates,
}
await this._setMCPUserStateOfName(newMCPServerStates)
this._metricsService.capture('Add MCP Servers', { servers: Object.keys(newMCPStates).join(', ') });
}
removeMCPUserStateOfNames = async (serverNames: string[]) => {
const { mcpUserStateOfName: mcpServerStates } = this.state
const newMCPServerStates = {
...mcpServerStates,
}
serverNames.forEach(serverName => {
if (serverName in newMCPServerStates) {
delete newMCPServerStates[serverName]
}
})
await this._setMCPUserStateOfName(newMCPServerStates)
this._metricsService.capture('Remove MCP Servers', { servers: serverNames.join(', ') });
}
setMCPServerState = async (serverName: string, state: MCPUserState) => {
const { mcpUserStateOfName } = this.state
const newMCPServerStates = {
...mcpUserStateOfName,
[serverName]: state,
}
await this._setMCPUserStateOfName(newMCPServerStates)
this._metricsService.capture('Update MCP Server State', { serverName, state });
}
}
@@ -4,7 +4,7 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { defaultModelsOfProvider, defaultProviderSettings } from './modelCapabilities.js';
import { defaultModelsOfProvider, defaultProviderSettings, ModelOverrides } from './modelCapabilities.js';
import { ToolApprovalType } from './toolsServiceTypes.js';
import { VoidSettingsState } from './voidSettingsService.js'
@@ -33,7 +33,7 @@ export type VoidStatefulModelInfo = { // <-- STATEFUL
modelName: string,
type: 'default' | 'autodetected' | 'custom';
isHidden: boolean, // whether or not the user is hiding it (switched off)
} // TODO!!! eventually we'd want to let the user change supportsFIM, etc on the model themselves
}
@@ -92,17 +92,20 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
return { title: 'Groq', }
}
else if (providerName === 'xAI') {
return { title: 'xAI', }
return { title: 'Grok (xAI)', }
}
else if (providerName === 'mistral') {
return { title: 'Mistral', }
}
// else if (providerName === 'googleVertex') {
// return { title: 'Google Vertex AI', }
// }
else if (providerName === 'googleVertex') {
return { title: 'Google Vertex AI', }
}
else if (providerName === 'microsoftAzure') {
return { title: 'Microsoft Azure OpenAI', }
}
else if (providerName === 'awsBedrock') {
return { title: 'AWS Bedrock', }
}
throw new Error(`descOfProviderName: Unknown provider name: "${providerName}"`)
}
@@ -112,17 +115,18 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
if (providerName === 'anthropic') return 'Get your [API Key here](https://console.anthropic.com/settings/keys).'
if (providerName === 'openAI') return 'Get your [API Key here](https://platform.openai.com/api-keys).'
if (providerName === 'deepseek') return 'Get your [API Key here](https://platform.deepseek.com/api_keys).'
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys).'
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey).'
if (providerName === 'openRouter') return 'Get your [API Key here](https://openrouter.ai/settings/keys). Read about [rate limits here](https://openrouter.ai/docs/api-reference/limits).'
if (providerName === 'gemini') return 'Get your [API Key here](https://aistudio.google.com/apikey). Read about [rate limits here](https://ai.google.dev/gemini-api/docs/rate-limits#current-rate-limits).'
if (providerName === 'groq') return 'Get your [API Key here](https://console.groq.com/keys).'
if (providerName === 'xAI') return 'Get your [API Key here](https://console.x.ai).'
if (providerName === 'mistral') return 'Get your [API Key here](https://console.mistral.ai/api-keys).'
if (providerName === 'openAICompatible') return `Use any OpenAI-compatible endpoint (LM Studio, LiteLM, etc).`
// if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'openAICompatible') return `Use any provider that's OpenAI-compatible (use this for llama.cpp and more).`
if (providerName === 'googleVertex') return 'You must authenticate before using Vertex with Void. Read more about endpoints [here](https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library), and regions [here](https://cloud.google.com/vertex-ai/docs/general/locations#available-regions).'
if (providerName === 'microsoftAzure') return 'Read more about endpoints [here](https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP), and get your API key [here](https://learn.microsoft.com/en-us/azure/search/search-security-api-keys?tabs=rest-use%2Cportal-find%2Cportal-query#find-existing-keys).'
if (providerName === 'ollama') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
if (providerName === 'vLLM') return 'If you would like to change this endpoint, please read more about [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
if (providerName === 'lmStudio') return 'If you would like to change this endpoint, please more about [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
if (providerName === 'awsBedrock') return 'Connect via a LiteLLM proxy or the AWS [Bedrock-Access-Gateway](https://github.com/aws-samples/bedrock-access-gateway). LiteLLM Bedrock setup docs are [here](https://docs.litellm.ai/docs/providers/bedrock).'
if (providerName === 'ollama') return 'Read more about custom [Endpoints here](https://github.com/ollama/ollama/blob/main/docs/faq.md#how-can-i-expose-ollama-on-my-network).'
if (providerName === 'vLLM') return 'Read more about custom [Endpoints here](https://docs.vllm.ai/en/latest/getting_started/quickstart.html#openai-compatible-server).'
if (providerName === 'lmStudio') return 'Read more about custom [Endpoints here](https://lmstudio.ai/docs/app/api/endpoints/openai).'
if (providerName === 'liteLLM') return 'Read more about endpoints [here](https://docs.litellm.ai/docs/providers/openai_compatible).'
throw new Error(`subTextMdOfProviderName: Unknown provider name: "${providerName}"`)
@@ -149,9 +153,10 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'openAICompatible' ? 'sk-key...' :
providerName === 'xAI' ? 'xai-key...' :
providerName === 'mistral' ? 'api-key...' :
// providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
providerName === 'awsBedrock' ? 'key-...' :
'',
isPasswordField: true,
}
@@ -162,29 +167,36 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'vLLM' ? 'Endpoint' :
providerName === 'lmStudio' ? 'Endpoint' :
providerName === 'openAICompatible' ? 'baseURL' : // (do not include /chat/completions)
// providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
providerName === 'awsBedrock' ? 'Endpoint' :
'(never)',
placeholder: providerName === 'ollama' ? defaultProviderSettings.ollama.endpoint
: providerName === 'vLLM' ? defaultProviderSettings.vLLM.endpoint
: providerName === 'openAICompatible' ? 'https://my-website.com/v1'
: providerName === 'lmStudio' ? defaultProviderSettings.lmStudio.endpoint
: providerName === 'liteLLM' ? 'http://localhost:4000'
: '(never)',
: providerName === 'awsBedrock' ? 'http://localhost:4000/v1'
: '(never)',
}
}
// else if (settingName === 'region') {
// // vertex only
// return {
// title: 'Region',
// placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
// : ''
// }
// }
else if (settingName === 'headersJSON') {
return { title: 'Custom Headers', placeholder: '{ "X-Request-Id": "..." }' }
}
else if (settingName === 'region') {
// vertex only
return {
title: 'Region',
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
: providerName === 'awsBedrock'
? defaultProviderSettings.awsBedrock.region
: ''
}
}
else if (settingName === 'azureApiVersion') {
// azure only
return {
@@ -196,11 +208,11 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
else if (settingName === 'project') {
return {
title: providerName === 'microsoftAzure' ? 'Resource'
// : providerName === 'googleVertex' ? 'Project'
: '',
: providerName === 'googleVertex' ? 'Project'
: '',
placeholder: providerName === 'microsoftAzure' ? 'my-resource'
// : providerName === 'googleVertex' ? 'my-project'
: ''
: providerName === 'googleVertex' ? 'my-project'
: ''
}
@@ -219,18 +231,16 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
}
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
// region: undefined, // googleVertex
region: undefined, // googleVertex
project: undefined,
azureApiVersion: undefined,
headersJSON: undefined,
}
@@ -324,18 +334,24 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.vLLM),
_didFillInProviderSettings: undefined,
},
// googleVertex: { // aggregator (serves models from multiple providers)
// ...defaultCustomSettings,
// ...defaultProviderSettings.googleVertex,
// ...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
// _didFillInProviderSettings: undefined,
// },
googleVertex: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.googleVertex,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.googleVertex),
_didFillInProviderSettings: undefined,
},
microsoftAzure: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.microsoftAzure,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
_didFillInProviderSettings: undefined,
},
awsBedrock: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.awsBedrock,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
_didFillInProviderSettings: undefined,
},
}
@@ -346,7 +362,7 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) =>
}
// this is a state
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply'] as const
export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply', 'SCM'] as const
export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null>
export type FeatureName = keyof ModelSelectionOfFeature
@@ -361,6 +377,9 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => {
return 'Chat'
else if (featureName === 'Apply')
return 'Apply'
// source control:
else if (featureName === 'SCM')
return 'Commit Message Generator'
else
throw new Error(`Feature Name ${featureName} not allowed`)
}
@@ -424,12 +443,15 @@ export type GlobalSettings = {
aiInstructions: string;
enableAutocomplete: boolean;
syncApplyToChat: boolean;
syncSCMToChat: boolean;
enableFastApply: boolean;
chatMode: ChatMode;
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
showInlineSuggestions: boolean;
includeToolLintErrors: boolean;
isOnboardingComplete: boolean;
disableSystemMessage: boolean;
autoAcceptLLMChanges: boolean;
}
export const defaultGlobalSettings: GlobalSettings = {
@@ -437,12 +459,15 @@ export const defaultGlobalSettings: GlobalSettings = {
aiInstructions: '',
enableAutocomplete: false,
syncApplyToChat: true,
syncSCMToChat: true,
enableFastApply: true,
chatMode: 'agent',
autoApprove: {},
showInlineSuggestions: true,
includeToolLintErrors: true,
isOnboardingComplete: false,
disableSystemMessage: false,
autoAcceptLLMChanges: false,
}
export type GlobalSettingName = keyof GlobalSettings
@@ -462,13 +487,38 @@ export const globalSettingNames = Object.keys(defaultGlobalSettings) as GlobalSe
export type ModelSelectionOptions = {
reasoningEnabled?: boolean;
reasoningBudget?: number;
reasoningEffort?: string;
}
export type OptionsOfModelSelection = {
[featureName in FeatureName]: Partial<{
[providerName in ProviderName]: {
[modelName: string]:
ModelSelectionOptions | undefined
[modelName: string]: ModelSelectionOptions | undefined
}
}>
}
export type OverridesOfModel = {
[providerName in ProviderName]: {
[modelName: string]: Partial<ModelOverrides> | undefined
}
}
const overridesOfModel = {} as OverridesOfModel
for (const providerName of providerNames) { overridesOfModel[providerName] = {} }
export const defaultOverridesOfModel = overridesOfModel
export interface MCPUserStateOfName {
[serverName: string]: MCPUserState | undefined;
}
export interface MCPUserState {
isOn: boolean;
}
@@ -5,8 +5,9 @@
import { generateUuid } from '../../../../../base/common/uuid.js'
import { endsWithAnyPrefixOf, SurroundingsRemover } from '../../common/helpers/extractCodeFromResult.js'
import { availableTools, InternalToolInfo, ToolName, ToolParamName } from '../../common/prompt/prompts.js'
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js'
import { OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js'
import { ToolName, ToolParamName } from '../../common/toolsServiceTypes.js'
import { ChatMode } from '../../common/voidSettingsTypes.js'
@@ -23,6 +24,9 @@ export const extractReasoningWrapper = (
let fullTextSoFar = ''
let fullReasoningSoFar = ''
if (!thinkTags[0] || !thinkTags[1]) throw new Error(`thinkTags must not be empty if provided. Got ${JSON.stringify(thinkTags)}.`)
let onText_ = onText
onText = (params) => {
onText_(params)
@@ -161,15 +165,15 @@ const findIndexOfAny = (fullText: string, matches: string[]) => {
type ToolOfToolName = { [toolName: string]: InternalToolInfo | undefined }
const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
const parseXMLPrefixToToolCall = <T extends ToolName,>(toolName: T, toolId: string, str: string, toolOfToolName: ToolOfToolName): RawToolCallObj => {
const paramsObj: RawToolParamsObj = {}
const doneParams: ToolParamName[] = []
const doneParams: ToolParamName<T>[] = []
let isDone = false
const getAnswer = (): RawToolCallObj => {
// trim off all whitespace at and before first \n and after last \n for each param
for (const p in paramsObj) {
const paramName = p as ToolParamName
const paramName = p as ToolParamName<T>
const orig = paramsObj[paramName]
if (orig === undefined) continue
paramsObj[paramName] = trimBeforeAndAfterNewLines(orig)
@@ -199,16 +203,16 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
const pm = new SurroundingsRemover(str)
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName[]
const allowedParams = Object.keys(toolOfToolName[toolName]?.params ?? {}) as ToolParamName<T>[]
if (allowedParams.length === 0) return getAnswer()
let latestMatchedOpenParam: null | ToolParamName = null
let latestMatchedOpenParam: null | ToolParamName<T> = null
let n = 0
while (true) {
n += 1
if (n > 10) return getAnswer() // just for good measure as this code is early
// find the param name opening tag
let matchedOpenParam: null | ToolParamName = null
let matchedOpenParam: null | ToolParamName<T> = null
for (const paramName of allowedParams) {
const removed = pm.removeFromStartUntilFullMatch(`<${paramName}>`, true)
if (removed) {
@@ -257,11 +261,14 @@ const parseXMLPrefixToToolCall = (toolName: ToolName, toolId: string, str: strin
}
export const extractXMLToolsWrapper = (
onText: OnText, onFinalMessage: OnFinalMessage, chatMode: ChatMode | null
onText: OnText,
onFinalMessage: OnFinalMessage,
chatMode: ChatMode | null,
mcpTools: InternalToolInfo[] | undefined,
): { newOnText: OnText, newOnFinalMessage: OnFinalMessage } => {
if (!chatMode) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
const tools = availableTools(chatMode)
const tools = availableTools(chatMode, mcpTools)
if (!tools) return { newOnText: onText, newOnFinalMessage: onFinalMessage }
const toolOfToolName: ToolOfToolName = {}
@@ -7,17 +7,27 @@
/* eslint-disable */
import Anthropic from '@anthropic-ai/sdk';
import { Ollama } from 'ollama';
import OpenAI, { ClientOptions } from 'openai';
import OpenAI, { ClientOptions, AzureOpenAI } from 'openai';
import { MistralCore } from '@mistralai/mistralai/core.js';
import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js';
// import { GoogleAuth } from 'google-auth-library'
import { Tool as GeminiTool, FunctionDeclaration, GoogleGenAI, ThinkingConfig, Schema, Type } from '@google/genai';
import { GoogleAuth } from 'google-auth-library'
/* eslint-enable */
import { AnthropicLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js';
import { AnthropicLLMChatMessage, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText, RawToolCallObj, RawToolParamsObj } from '../../common/sendLLMMessageTypes.js';
import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, OverridesOfModel, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js';
import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getReservedOutputTokenSpace } from '../../common/modelCapabilities.js';
import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js';
import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js';
import { availableTools, InternalToolInfo } from '../../common/prompt/prompts.js';
import { generateUuid } from '../../../../../base/common/uuid.js';
const getGoogleApiKey = async () => {
// modulelevel singleton
const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
const key = await auth.getAccessToken()
if (!key) throw new Error(`Google API failed to generate a key.`)
return key
}
@@ -29,11 +39,17 @@ type InternalCommonMessageParams = {
providerName: ProviderName;
settingsOfProvider: SettingsOfProvider;
modelSelectionOptions: ModelSelectionOptions | undefined;
overridesOfModel: OverridesOfModel | undefined;
modelName: string;
_setAborter: (aborter: () => void) => void;
}
type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; }
type SendChatParams_Internal = InternalCommonMessageParams & {
messages: LLMChatMessage[];
separateSystemMessage: string | undefined;
chatMode: ChatMode | null;
mcpTools: InternalToolInfo[] | undefined;
}
type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; }
export type ListParams_Internal<ModelResponse> = ModelListParams<ModelResponse>
@@ -42,15 +58,17 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
// const getGoogleApiKey = async () => {
// // modulelevel singleton
// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` });
// const key = await auth.getAccessToken()
// if (!key) throw new Error(`Google API failed to generate a key.`)
// return key
// }
const parseHeadersJSON = (s: string | undefined): Record<string, string | null | undefined> | undefined => {
if (!s) return undefined
try {
return JSON.parse(s)
} catch (e) {
throw new Error(`Error parsing OpenAI-Compatible headers: ${s} is not a valid JSON.`)
}
}
const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => {
const commonPayloadOpts: ClientOptions = {
dangerouslyAllowBrowser: true,
@@ -88,22 +106,45 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
...commonPayloadOpts,
})
}
else if (providerName === 'gemini') {
else if (providerName === 'googleVertex') {
// https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts })
const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
const apiKey = await getGoogleApiKey()
return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
}
// else if (providerName === 'googleVertex') {
// // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library
// const thisConfig = settingsOfProvider[providerName]
// const baseURL = `https://${thisConfig.region}-aiplatform.googleapis.com/v1/projects/${thisConfig.project}/locations/${thisConfig.region}/endpoints/${'openapi'}`
// return new OpenAI({ baseURL: baseURL, apiKey: apiKey, ...commonPayloadOpts })
// }
else if (providerName === 'microsoftAzure') {
// https://learn.microsoft.com/en-us/rest/api/aifoundry/model-inference/get-chat-completions/get-chat-completions?view=rest-aifoundry-model-inference-2024-05-01-preview&tabs=HTTP
// https://github.com/openai/openai-node?tab=readme-ov-file#microsoft-azure-openai
const thisConfig = settingsOfProvider[providerName]
const baseURL = `https://${thisConfig.project}.services.ai.azure.com/api/models/chat/completions??api-version=${thisConfig.azureApiVersion}`
return new OpenAI({ baseURL: baseURL, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
const endpoint = `https://${thisConfig.project}.openai.azure.com/`;
const apiVersion = thisConfig.azureApiVersion ?? '2024-04-01-preview';
const options = { endpoint, apiKey: thisConfig.apiKey, apiVersion };
return new AzureOpenAI({ ...options, ...commonPayloadOpts });
}
else if (providerName === 'awsBedrock') {
/**
* We treat Bedrock as *OpenAI-compatible only through a proxy*:
* LiteLLM default http://localhost:4000/v1
* Bedrock-Access-Gateway https://<api-id>.execute-api.<region>.amazonaws.com/openai/
*
* The native Bedrock runtime endpoint
* https://bedrock-runtime.<region>.amazonaws.com
* is **NOT** OpenAI-compatible, so we do *not* fall back to it here.
*/
const { endpoint, apiKey } = settingsOfProvider.awsBedrock
// ① use the user-supplied proxy if present
// ② otherwise default to local LiteLLM
let baseURL = endpoint || 'http://localhost:4000/v1'
// Normalize: make sure we end with “/v1”
if (!baseURL.endsWith('/v1'))
baseURL = baseURL.replace(/\/+$/, '') + '/v1'
return new OpenAI({ baseURL, apiKey, ...commonPayloadOpts })
}
else if (providerName === 'deepseek') {
const thisConfig = settingsOfProvider[providerName]
@@ -111,7 +152,8 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
}
else if (providerName === 'openAICompatible') {
const thisConfig = settingsOfProvider[providerName]
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, ...commonPayloadOpts })
const headers = parseHeadersJSON(thisConfig.headersJSON)
return new OpenAI({ baseURL: thisConfig.endpoint, apiKey: thisConfig.apiKey, defaultHeaders: headers, ...commonPayloadOpts })
}
else if (providerName === 'groq') {
const thisConfig = settingsOfProvider[providerName]
@@ -130,8 +172,14 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
}
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, overridesOfModel }: SendFIMParams_Internal) => {
const {
modelName,
supportsFIM,
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
@@ -140,7 +188,7 @@ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens
return
}
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider })
const openai = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload: additionalOpenAIPayload })
openai.completions
.create({
model: modelName,
@@ -182,8 +230,8 @@ const toOpenAICompatibleTool = (toolInfo: InternalToolInfo) => {
} satisfies OpenAI.Chat.Completions.ChatCompletionTool
}
const openAITools = (chatMode: ChatMode) => {
const allowedTools = availableTools(chatMode)
const openAITools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
const allowedTools = availableTools(chatMode, mcpTools)
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
const openAITools: OpenAI.Chat.Completions.ChatCompletionTool[] = []
@@ -193,55 +241,72 @@ const openAITools = (chatMode: ChatMode) => {
return openAITools
}
const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
if (!isAToolName(name)) return null
const rawParams: RawToolParamsObj = {}
// convert LLM tool call to our tool format
const rawToolCallObjOfParamsStr = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => {
let input: unknown
try {
input = JSON.parse(toolParamsStr)
}
catch (e) {
return null
}
try { input = JSON.parse(toolParamsStr) }
catch (e) { return null }
if (input === null) return null
if (typeof input !== 'object') return null
for (const paramName in voidTools[name].params) {
rawParams[paramName as ToolParamName] = (input as any)[paramName]
}
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
const rawParams: RawToolParamsObj = input
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
}
const rawToolCallObjOfAnthropicParams = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
const { id, name, input } = toolBlock
if (input === null) return null
if (typeof input !== 'object') return null
const rawParams: RawToolParamsObj = input
return { id, name, rawParams, doneParams: Object.keys(rawParams), isDone: true }
}
// ------------ OPENAI-COMPATIBLE ------------
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage }: SendChatParams_Internal) => {
const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, providerName, chatMode, separateSystemMessage, overridesOfModel, mcpTools }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_)
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
const { canIOReasoning, openSourceThinkTags } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
const includeInPayload = {
...providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo),
...additionalOpenAIPayload
}
// tools
const potentialTools = chatMode !== null ? openAITools(chatMode) : null
const potentialTools = openAITools(chatMode, mcpTools)
const nativeToolsObj = potentialTools && specialToolFormat === 'openai-style' ?
{ tools: potentialTools } as const
: {}
// instance
const openai: OpenAI = await newOpenAICompatibleSDK({ providerName, settingsOfProvider, includeInPayload })
if (providerName === 'microsoftAzure') {
// Required to select the model
(openai as AzureOpenAI).deploymentName = modelName;
}
const options: OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming = {
model: modelName,
messages: messages as any,
stream: true,
...nativeToolsObj,
...additionalOpenAIPayload
// max_completion_tokens: maxTokens,
}
@@ -256,7 +321,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@@ -297,10 +362,11 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
fullReasoningSoFar += newReasoning
}
// call onText
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId } : undefined,
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
})
}
@@ -309,7 +375,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId)
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
@@ -375,8 +441,8 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => {
} satisfies Anthropic.Messages.Tool
}
const anthropicTools = (chatMode: ChatMode) => {
const allowedTools = availableTools(chatMode)
const anthropicTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined) => {
const allowedTools = availableTools(chatMode, mcpTools)
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
const anthropicTools: Anthropic.Messages.ToolUnion[] = []
@@ -386,37 +452,27 @@ const anthropicTools = (chatMode: ChatMode) => {
return anthropicTools
}
const anthropicToolToRawToolCallObj = (toolBlock: Anthropic.Messages.ToolUseBlock): RawToolCallObj | null => {
const { id, name, input } = toolBlock
if (!isAToolName(name)) return null
const rawParams: RawToolParamsObj = {}
if (input === null) return null
if (typeof input !== 'object') return null
for (const paramName in voidTools[name].params) {
rawParams[paramName as ToolParamName] = (input as any)[paramName]
}
return { id, name, rawParams, doneParams: Object.keys(rawParams) as ToolParamName[], isDone: true }
}
// ------------ ANTHROPIC ------------
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, separateSystemMessage, chatMode }: SendChatParams_Internal) => {
const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName: modelName_, _setAborter, separateSystemMessage, chatMode, mcpTools }: SendChatParams_Internal) => {
const {
modelName,
specialToolFormat,
} = getModelCapabilities(providerName, modelName_)
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
const thisConfig = settingsOfProvider.anthropic
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
// anthropic-specific - max tokens
const maxTokens = getMaxOutputTokens(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled })
const maxTokens = getReservedOutputTokenSpace(providerName, modelName_, { isReasoningEnabled: !!reasoningInfo?.isReasoningEnabled, overridesOfModel })
// tools
const potentialTools = chatMode !== null ? anthropicTools(chatMode) : null
const potentialTools = anthropicTools(chatMode, mcpTools)
const nativeToolsObj = potentialTools && specialToolFormat === 'anthropic-style' ?
{ tools: potentialTools, tool_choice: { type: 'auto' } } as const
: {}
@@ -438,9 +494,9 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
})
// manually parse out tool results
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode)
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
@@ -457,7 +513,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
onText({
fullText,
fullReasoning,
toolCall: isAToolName(fullToolName) ? { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
toolCall: !fullToolName ? undefined : { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
})
}
// there are no events for tool_use, it comes in at the end
@@ -507,8 +563,11 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
stream.on('finalMessage', (response) => {
const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking')
const tools = response.content.filter(c => c.type === 'tool_use')
const toolCall = tools[0] && anthropicToolToRawToolCallObj(tools[0])
// console.log('TOOLS!!!!!!', JSON.stringify(tools, null, 2))
// console.log('TOOLS!!!!!!', JSON.stringify(response, null, 2))
const toolCall = tools[0] && rawToolCallObjOfAnthropicParams(tools[0])
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText, fullReasoning, anthropicReasoning, ...toolCallObj })
})
// on error
@@ -523,8 +582,8 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
// ------------ MISTRAL ------------
// https://docs.mistral.ai/api/#tag/fim
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_)
const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, overridesOfModel, modelName: modelName_, _setAborter, providerName }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_, overridesOfModel)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
@@ -544,6 +603,7 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
stop: messages.stopTokens,
})
.then(async response => {
// unfortunately, _setAborter() does not exist
let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : '';
const fullText = typeof content === 'string' ? content
@@ -620,6 +680,170 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
})
}
// ---------------- GEMINI NATIVE IMPLEMENTATION ----------------
const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
return {
name,
description,
parameters: {
type: Type.OBJECT,
properties: Object.entries(params).reduce((acc, [key, value]) => {
acc[key] = {
type: Type.STRING,
description: value.description
};
return acc;
}, {} as Record<string, Schema>)
}
} satisfies FunctionDeclaration
}
const geminiTools = (chatMode: ChatMode | null, mcpTools: InternalToolInfo[] | undefined): GeminiTool[] | null => {
const allowedTools = availableTools(chatMode, mcpTools)
if (!allowedTools || Object.keys(allowedTools).length === 0) return null
const functionDecls: FunctionDeclaration[] = []
for (const t in allowedTools ?? {}) {
functionDecls.push(toGeminiFunctionDecl(allowedTools[t]))
}
const tools: GeminiTool = { functionDeclarations: functionDecls, }
return [tools]
}
// Implementation for Gemini using Google's native API
const sendGeminiChat = async ({
messages,
separateSystemMessage,
onText,
onFinalMessage,
onError,
settingsOfProvider,
overridesOfModel,
modelName: modelName_,
_setAborter,
providerName,
modelSelectionOptions,
chatMode,
mcpTools,
}: SendChatParams_Internal) => {
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
const thisConfig = settingsOfProvider[providerName]
const {
modelName,
specialToolFormat,
// reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
// const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// reasoning
// const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
// const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
const thinkingConfig: ThinkingConfig | undefined = !reasoningInfo?.isReasoningEnabled ? undefined
: reasoningInfo.type === 'budget_slider_value' ?
{ thinkingBudget: reasoningInfo.reasoningBudget }
: undefined
// tools
const potentialTools = geminiTools(chatMode, mcpTools)
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
potentialTools
: undefined
// instance
const genAI = new GoogleGenAI({ apiKey: thisConfig.apiKey });
// manually parse out tool results if XML
if (!specialToolFormat) {
const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode, mcpTools)
onText = newOnText
onFinalMessage = newOnFinalMessage
}
// when receive text
let fullReasoningSoFar = ''
let fullTextSoFar = ''
let toolName = ''
let toolParamsStr = ''
let toolId = ''
genAI.models.generateContentStream({
model: modelName,
config: {
systemInstruction: separateSystemMessage,
thinkingConfig: thinkingConfig,
tools: toolConfig,
},
contents: messages as GeminiLLMChatMessage[],
})
.then(async (stream) => {
_setAborter(() => { stream.return(fullTextSoFar); });
// Process the stream
for await (const chunk of stream) {
// message
const newText = chunk.text ?? ''
fullTextSoFar += newText
// tool call
const functionCalls = chunk.functionCalls
if (functionCalls && functionCalls.length > 0) {
const functionCall = functionCalls[0] // Get the first function call
toolName = functionCall.name ?? ''
toolParamsStr = JSON.stringify(functionCall.args ?? {})
toolId = functionCall.id ?? ''
}
// (do not handle reasoning yet)
// call onText
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
})
}
// on final
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
} else {
if (!toolId) toolId = generateUuid() // ids are empty, but other providers might expect an id
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
})
.catch(error => {
const message = error?.message
if (typeof message === 'string') {
if (error.message?.includes('API key')) {
onError({ message: invalidApiKeyMessage(providerName), fullError: error });
}
else if (error?.message?.includes('429')) {
onError({ message: 'Rate limit reached. ' + error, fullError: error });
}
else
onError({ message: error + '', fullError: error });
}
else {
onError({ message: error + '', fullError: error });
}
})
};
type CallFnOfProvider = {
@@ -647,7 +871,7 @@ export const sendLLMMessageToProviderImplementation = {
list: null,
},
gemini: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendChat: (params) => sendGeminiChat(params),
sendFIM: null,
list: null,
},
@@ -688,25 +912,32 @@ export const sendLLMMessageToProviderImplementation = {
},
lmStudio: {
// lmStudio has no suffix parameter in /completions, so sendFIM might not work
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null, // lmStudio has no suffix parameter in /completions
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
list: (params) => _openaiCompatibleList(params),
},
liteLLM: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: (params) => _sendOpenAICompatibleFIM(params),
list: null,
},
googleVertex: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
// googleVertex: {
// sendChat: (params) => _sendOpenAICompatibleChat(params),
// sendFIM: null,
// list: null,
// },
microsoftAzure: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
awsBedrock: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
} satisfies CallFnOfProvider
@@ -20,8 +20,10 @@ export const sendLLMMessage = async ({
settingsOfProvider,
modelSelection,
modelSelectionOptions,
overridesOfModel,
chatMode,
separateSystemMessage,
mcpTools,
}: SendLLMMessageParams,
metricsService: IMetricsService
@@ -33,13 +35,6 @@ export const sendLLMMessage = async ({
// only captures number of messages and message "shape", no actual code, instructions, prompts, etc
const captureLLMEvent = (eventId: string, extras?: object) => {
let totalTokens = 0
if (messagesType === 'chatMessages') {
for (const m of messages_) totalTokens += m.content.length
}
else {
totalTokens = messages_.prefix.length + messages_.suffix.length
}
metricsService.capture(eventId, {
providerName,
@@ -48,13 +43,10 @@ export const sendLLMMessage = async ({
numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length,
...messagesType === 'chatMessages' ? {
numMessages: messages_?.length,
messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })),
} : messagesType === 'FIMMessage' ? {
prefixLength: messages_.prefix.length,
suffixLength: messages_.suffix.length,
} : {},
totalTokens,
...loggingExtras,
...extras,
})
@@ -74,9 +66,9 @@ export const sendLLMMessage = async ({
}
const onFinalMessage: OnFinalMessage = (params) => {
const { fullText, fullReasoning } = params
const { fullText, fullReasoning, toolCall } = params
if (_didAbort) return
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds() })
captureLLMEvent(`${loggingName} - Received Full Message`, { messageLength: fullText.length, reasoningLength: fullReasoning?.length, duration: new Date().getMilliseconds() - submit_time.getMilliseconds(), toolCallName: toolCall?.name })
onFinalMessage_(params)
}
@@ -103,7 +95,7 @@ export const sendLLMMessage = async ({
if (messagesType === 'chatMessages')
captureLLMEvent(`${loggingName} - Sending Message`, { userMessageLength: messages_?.[messages_.length - 1]?.content.length })
captureLLMEvent(`${loggingName} - Sending Message`, {})
else if (messagesType === 'FIMMessage')
captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length })
@@ -116,15 +108,15 @@ export const sendLLMMessage = async ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage, chatMode })
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage, chatMode, mcpTools })
return
}
if (messagesType === 'FIMMessage') {
if (sendFIM) {
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, separateSystemMessage })
await sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, modelName, _setAborter, providerName, separateSystemMessage })
return
}
onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null })
onError({ message: `Error running Autocomplete with ${providerName} - ${modelName}.`, fullError: null })
return
}
onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null })
@@ -0,0 +1,395 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
// registered in app.ts
// can't make a service responsible for this, because it needs
// to be connected to the main process and node dependencies
import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js';
import { Emitter, Event } from '../../../../base/common/event.js';
import { Client } from '@modelcontextprotocol/sdk/client/index.js';
import { StdioClientTransport } from '@modelcontextprotocol/sdk/client/stdio.js';
import { StreamableHTTPClientTransport } from '@modelcontextprotocol/sdk/client/streamableHttp.js';
import { SSEClientTransport } from '@modelcontextprotocol/sdk/client/sse.js';
import { MCPConfigFileJSON, MCPConfigFileEntryJSON, MCPServer, RawMCPToolCall, MCPToolErrorResponse, MCPServerEventResponse, MCPToolCallParams, removeMCPToolNamePrefix } from '../common/mcpServiceTypes.js';
import { Transport } from '@modelcontextprotocol/sdk/shared/transport.js';
import { CallToolResult } from '@modelcontextprotocol/sdk/types.js';
import { MCPUserStateOfName } from '../common/voidSettingsTypes.js';
const getClientConfig = (serverName: string) => {
return {
name: `${serverName}-client`,
version: '0.1.0',
// debug: true,
}
}
type MCPServerNonError = MCPServer & { status: Omit<MCPServer['status'], 'error'> }
type MCPServerError = MCPServer & { status: 'error' }
type ClientInfo = {
_client: Client, // _client is the client that connects with an mcp client. We're calling mcp clients "server" everywhere except here for naming consistency.
mcpServerEntryJSON: MCPConfigFileEntryJSON,
mcpServer: MCPServerNonError,
} | {
_client?: undefined,
mcpServerEntryJSON: MCPConfigFileEntryJSON,
mcpServer: MCPServerError,
}
type InfoOfClientId = {
[clientId: string]: ClientInfo
}
export class MCPChannel implements IServerChannel {
private readonly infoOfClientId: InfoOfClientId = {}
private readonly _refreshingServerNames: Set<string> = new Set()
// mcp emitters
private readonly mcpEmitters = {
serverEvent: {
onAdd: new Emitter<MCPServerEventResponse>(),
onUpdate: new Emitter<MCPServerEventResponse>(),
onDelete: new Emitter<MCPServerEventResponse>(),
}
} satisfies {
serverEvent: {
onAdd: Emitter<MCPServerEventResponse>,
onUpdate: Emitter<MCPServerEventResponse>,
onDelete: Emitter<MCPServerEventResponse>,
}
}
constructor(
) { }
// browser uses this to listen for changes
listen(_: unknown, event: string): Event<any> {
// server events
if (event === 'onAdd_server') return this.mcpEmitters.serverEvent.onAdd.event;
else if (event === 'onUpdate_server') return this.mcpEmitters.serverEvent.onUpdate.event;
else if (event === 'onDelete_server') return this.mcpEmitters.serverEvent.onDelete.event;
// else if (event === 'onLoading_server') return this.mcpEmitters.serverEvent.onChangeLoading.event;
// tool call events
// handle unknown events
else throw new Error(`Event not found: ${event}`);
}
// browser uses this to call (see this.channel.call() in mcpConfigService.ts for all usages)
async call(_: unknown, command: string, params: any): Promise<any> {
try {
if (command === 'refreshMCPServers') {
await this._refreshMCPServers(params)
}
else if (command === 'closeAllMCPServers') {
await this._closeAllMCPServers()
}
else if (command === 'toggleMCPServer') {
await this._toggleMCPServer(params.serverName, params.isOn)
}
else if (command === 'callTool') {
const p: MCPToolCallParams = params
const response = await this._safeCallTool(p.serverName, p.toolName, p.params)
return response
}
else {
throw new Error(`Void sendLLM: command "${command}" not recognized.`)
}
}
catch (e) {
console.error('mcp channel: Call Error:', e)
}
}
// server functions
private async _refreshMCPServers(params: { mcpConfigFileJSON: MCPConfigFileJSON, userStateOfName: MCPUserStateOfName, addedServerNames: string[], removedServerNames: string[], updatedServerNames: string[] }) {
const {
mcpConfigFileJSON,
userStateOfName,
addedServerNames,
removedServerNames,
updatedServerNames,
} = params
const { mcpServers: mcpServersJSON } = mcpConfigFileJSON
const allChanges: { type: 'added' | 'removed' | 'updated', serverName: string }[] = [
...addedServerNames.map(n => ({ serverName: n, type: 'added' }) as const),
...removedServerNames.map(n => ({ serverName: n, type: 'removed' }) as const),
...updatedServerNames.map(n => ({ serverName: n, type: 'updated' }) as const),
]
await Promise.all(
allChanges.map(async ({ serverName, type }) => {
// check if already refreshing
if (this._refreshingServerNames.has(serverName)) return
this._refreshingServerNames.add(serverName)
const prevServer = this.infoOfClientId[serverName]?.mcpServer;
// close and delete the old client
if (type === 'removed' || type === 'updated') {
await this._closeClient(serverName)
delete this.infoOfClientId[serverName]
this.mcpEmitters.serverEvent.onDelete.fire({ response: { prevServer, name: serverName, } })
}
// create a new client
if (type === 'added' || type === 'updated') {
const clientInfo = await this._createClient(mcpServersJSON[serverName], serverName, userStateOfName[serverName]?.isOn)
this.infoOfClientId[serverName] = clientInfo
this.mcpEmitters.serverEvent.onAdd.fire({ response: { newServer: clientInfo.mcpServer, name: serverName, } })
}
})
)
allChanges.forEach(({ serverName, type }) => {
this._refreshingServerNames.delete(serverName)
})
}
private async _createClientUnsafe(server: MCPConfigFileEntryJSON, serverName: string, isOn: boolean): Promise<ClientInfo> {
const clientConfig = getClientConfig(serverName)
const client = new Client(clientConfig)
let transport: Transport;
let info: MCPServerNonError;
if (server.url) {
// first try HTTP, fall back to SSE
try {
transport = new StreamableHTTPClientTransport(server.url);
await client.connect(transport);
console.log(`Connected via HTTP to ${serverName}`);
const { tools } = await client.listTools()
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
info = {
status: isOn ? 'success' : 'offline',
tools: toolsWithUniqueName,
command: server.url.toString(),
}
} catch (httpErr) {
console.warn(`HTTP failed for ${serverName}, trying SSE…`, httpErr);
transport = new SSEClientTransport(server.url);
await client.connect(transport);
const { tools } = await client.listTools()
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
console.log(`Connected via SSE to ${serverName}`);
info = {
status: isOn ? 'success' : 'offline',
tools: toolsWithUniqueName,
command: server.url.toString(),
}
}
} else if (server.command) {
// console.log('ENV DATA: ', server.env)
transport = new StdioClientTransport({
command: server.command,
args: server.args,
env: {
...server.env,
...process.env
} as Record<string, string>,
});
await client.connect(transport)
// Get the tools from the server
const { tools } = await client.listTools()
const toolsWithUniqueName = tools.map(({ name, ...rest }) => ({ name: this._addUniquePrefix(name), ...rest }))
// Create a full command string for display
const fullCommand = `${server.command} ${server.args?.join(' ') || ''}`
// Format server object
info = {
status: isOn ? 'success' : 'offline',
tools: toolsWithUniqueName,
command: fullCommand,
}
} else {
throw new Error(`No url or command for server ${serverName}`);
}
return { _client: client, mcpServerEntryJSON: server, mcpServer: info }
}
private _addUniquePrefix(base: string) {
return `${Math.random().toString(36).slice(2, 8)}_${base}`;
}
private async _createClient(serverConfig: MCPConfigFileEntryJSON, serverName: string, isOn = true): Promise<ClientInfo> {
try {
const c: ClientInfo = await this._createClientUnsafe(serverConfig, serverName, isOn)
return c
} catch (err) {
console.error(`❌ Failed to connect to server "${serverName}":`, err)
const fullCommand = !serverConfig.command ? '' : `${serverConfig.command} ${serverConfig.args?.join(' ') || ''}`
const c: MCPServerError = { status: 'error', error: err + '', command: fullCommand, }
return { mcpServerEntryJSON: serverConfig, mcpServer: c, }
}
}
private async _closeAllMCPServers() {
for (const serverName in this.infoOfClientId) {
await this._closeClient(serverName)
delete this.infoOfClientId[serverName]
}
console.log('Closed all MCP servers');
}
private async _closeClient(serverName: string) {
const info = this.infoOfClientId[serverName]
if (!info) return
const { _client: client } = info
if (client) {
await client.close()
}
console.log(`Closed MCP server ${serverName}`);
}
private async _toggleMCPServer(serverName: string, isOn: boolean) {
const prevServer = this.infoOfClientId[serverName]?.mcpServer
// Handle turning on the server
if (isOn) {
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
const clientInfo = await this._createClientUnsafe(this.infoOfClientId[serverName].mcpServerEntryJSON, serverName, isOn)
this.mcpEmitters.serverEvent.onUpdate.fire({
response: {
name: serverName,
newServer: clientInfo.mcpServer,
prevServer: prevServer,
}
})
}
// Handle turning off the server
else {
// this.mcpEmitters.serverEvent.onChangeLoading.fire(getLoadingServerObject(serverName, isOn))
this._closeClient(serverName)
delete this.infoOfClientId[serverName]._client
this.mcpEmitters.serverEvent.onUpdate.fire({
response: {
name: serverName,
newServer: {
status: 'offline',
tools: [],
command: '',
// Explicitly set error to undefined to reset the error state
error: undefined,
},
prevServer: prevServer,
}
})
}
}
// tool call functions
private async _callTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
const server = this.infoOfClientId[serverName]
if (!server) throw new Error(`Server ${serverName} not found`)
const { _client: client } = server
if (!client) throw new Error(`Client for server ${serverName} not found`)
// Call the tool with the provided parameters
const response = await client.callTool({
name: removeMCPToolNamePrefix(toolName),
arguments: params
})
const { content } = response as CallToolResult
const returnValue = content[0]
if (returnValue.type === 'text') {
// handle text response
if (response.isError) {
throw new Error(`Tool call error: ${returnValue.text}`)
}
// handle success
return {
event: 'text',
text: returnValue.text,
toolName,
serverName,
}
}
// if (returnValue.type === 'audio') {
// // handle audio response
// }
// if (returnValue.type === 'image') {
// // handle image response
// }
// if (returnValue.type === 'resource') {
// // handle resource response
// }
throw new Error(`Tool call error: We don\'t support ${returnValue.type} tool response yet for tool ${toolName} on server ${serverName}`)
}
// tool call error wrapper
private async _safeCallTool(serverName: string, toolName: string, params: any): Promise<RawMCPToolCall> {
try {
const response = await this._callTool(serverName, toolName, params)
return response
} catch (err) {
let errorMessage: string;
if (typeof err === 'object' && err !== null && err['code']) {
const code = err.code
let codeDescription = ''
if (code === -32700)
codeDescription = 'Parse Error';
if (code === -32600)
codeDescription = 'Invalid Request';
if (code === -32601)
codeDescription = 'Method Not Found';
if (code === -32602)
codeDescription = 'Invalid Parameters';
if (code === -32603)
codeDescription = 'Internal Error';
errorMessage = `${codeDescription}. Full response:\n${JSON.stringify(err, null, 2)}`
}
// Check if it's an MCP error with a code
else if (typeof err === 'string') {
// String error
errorMessage = err;
} else {
// Unknown error format
errorMessage = JSON.stringify(err, null, 2);
}
const fullErrorMessage = `❌ Failed to call tool "${toolName}" on server "${serverName}": ${errorMessage}`;
const errorResponse: MCPToolErrorResponse = {
event: 'error',
text: fullErrorMessage,
toolName,
serverName,
}
return errorResponse
}
}
}
@@ -13,6 +13,7 @@ import { IApplicationStorageMainService } from '../../../../platform/storage/ele
import { IMetricsService } from '../common/metricsService.js';
import { PostHog } from 'posthog-node'
import { OPT_OUT_KEY } from '../common/storageKeys.js';
const os = isWindows ? 'windows' : isMacintosh ? 'mac' : isLinux ? 'linux' : null
@@ -29,6 +30,8 @@ const osInfo = _getOSInfo()
// we'd like to use devDeviceId on telemetryService, but that gets sanitized by the time it gets here as 'someValue.devDeviceId'
export class MetricsMainService extends Disposable implements IMetricsService {
_serviceBrand: undefined;
@@ -119,7 +122,18 @@ export class MetricsMainService extends Disposable implements IMetricsService {
distinctId: this.distinctId,
properties: this._initProperties,
}
this.client.identify(identifyMessage)
const didOptOut = this._appStorage.getBoolean(OPT_OUT_KEY, StorageScope.APPLICATION, false)
console.log('User is opted out of basic Void metrics?', didOptOut)
if (didOptOut) {
this.client.optOut()
}
else {
this.client.optIn()
this.client.identify(identifyMessage)
}
console.log('Void posthog metrics info:', JSON.stringify(identifyMessage, null, 2))
}
@@ -127,10 +141,18 @@ export class MetricsMainService extends Disposable implements IMetricsService {
capture: IMetricsService['capture'] = (event, params) => {
const capture = { distinctId: this.distinctId, event, properties: params } as const
// console.log('full capture:', capture)
// console.log('full capture:', this.distinctId)
this.client.capture(capture)
}
setOptOut: IMetricsService['setOptOut'] = (newVal: boolean) => {
if (newVal) {
this._appStorage.store(OPT_OUT_KEY, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
}
else {
this._appStorage.remove(OPT_OUT_KEY, StorageScope.APPLICATION)
}
}
async getDebuggingProperties() {
return this._initProperties
@@ -0,0 +1,82 @@
/*--------------------------------------------------------------------------------------
* Copyright 2025 Glass Devtools, Inc. All rights reserved.
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { promisify } from 'util'
import { exec as _exec } from 'child_process'
import { IVoidSCMService } from '../common/voidSCMTypes.js'
interface NumStat {
file: string
added: number
removed: number
}
const exec = promisify(_exec)
//8000 and 10 were chosen after some experimentation on small-to-moderately sized changes
const MAX_DIFF_LENGTH = 8000
const MAX_DIFF_FILES = 10
const git = async (command: string, path: string): Promise<string> => {
const { stdout, stderr } = await exec(`${command}`, { cwd: path })
if (stderr) {
throw new Error(stderr)
}
return stdout.trim()
}
const getNumStat = async (path: string, useStagedChanges: boolean): Promise<NumStat[]> => {
const staged = useStagedChanges ? '--staged' : ''
const output = await git(`git diff --numstat ${staged}`, path)
return output
.split('\n')
.map((line) => {
const [added, removed, file] = line.split('\t')
return {
file,
added: parseInt(added, 10) || 0,
removed: parseInt(removed, 10) || 0,
}
})
}
const getSampledDiff = async (file: string, path: string, useStagedChanges: boolean): Promise<string> => {
const staged = useStagedChanges ? '--staged' : ''
const diff = await git(`git diff --unified=0 --no-color ${staged} -- "${file}"`, path)
return diff.slice(0, MAX_DIFF_LENGTH)
}
const hasStagedChanges = async (path: string): Promise<boolean> => {
const output = await git('git diff --staged --name-only', path)
return output.length > 0
}
export class VoidSCMService implements IVoidSCMService {
readonly _serviceBrand: undefined
async gitStat(path: string): Promise<string> {
const useStagedChanges = await hasStagedChanges(path)
const staged = useStagedChanges ? '--staged' : ''
return git(`git diff --stat ${staged}`, path)
}
async gitSampledDiffs(path: string): Promise<string> {
const useStagedChanges = await hasStagedChanges(path)
const numStatList = await getNumStat(path, useStagedChanges)
const topFiles = numStatList
.sort((a, b) => (b.added + b.removed) - (a.added + a.removed))
.slice(0, MAX_DIFF_FILES)
const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path, useStagedChanges) })))
return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n')
}
gitBranch(path: string): Promise<string> {
return git('git branch --show-current', path)
}
gitLog(path: string): Promise<string> {
return git('git log --pretty=format:"%h|%s|%ad" --date=short --no-merges -n 5', path)
}
}
@@ -32,6 +32,12 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
return { message: null } as const
}
// if disabled and not explicitly checking, return early
if (this._updateService.state.type === StateType.Disabled) {
if (!explicit)
return { message: null } as const
}
this._updateService.checkForUpdates(false) // implicity check, then handle result ourselves
console.log('updateState', this._updateService.state)
@@ -94,7 +100,7 @@ export class VoidMainUpdateService extends Disposable implements IVoidUpdateServ
const data = await response.json();
const version = data.tag_name;
const myVersion = `${this._productService.version}.${this._productService.release}`
const myVersion = this._productService.version
const latestVersion = version
const isUpToDate = myVersion === latestVersion // only makes sense if response.ok
Binary file not shown.

After

Width:  |  Height:  |  Size: 200 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 813 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 795 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 850 KiB