Compare commits

..

353 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 58d95035ff update message 2025-05-10 03:28:29 -07:00
Andrew Pareles 5232b4c30c update prompt to work better in agent 2025-05-10 02:28:40 -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
59 changed files with 4345 additions and 1147 deletions
+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 }}
+6
View File
@@ -8,3 +8,9 @@ Look for services and built-in functions that you might need to use to solve the
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.
+49 -62
View File
@@ -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,34 +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
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. `git clone https://github.com/voideditor/void` to clone the repo.
2. `npm install` to install all dependencies.
3. To build Void, open VSCode. Then:
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>.
- This step can take ~5 min. The build is done when you see two check marks (one of the items will continue spinning indefinitely - it compiles our React code).
4. To run Void:
4. Open the Void Developer Mode window:
- Windows: `./scripts/code.bat`.
- Mac: `./scripts/code.sh`.
- Linux: `./scripts/code.sh`.
5. 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).
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:
@@ -82,50 +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`)!
- You can do this easily without touching your base installation with [nvm](https://github.com/nvm-sh/nvm). Simply run `nvm install`, followed by `nvm use` and it will automatically install and use the version specified in `nvmrc`
- Make sure that 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 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
@@ -137,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
-->
+36 -12
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,29 +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. If you're new, welcome!
This repo contains the full sourcecode for Void's Desktop app. If you're new, welcome!
- 🧭 [Website](https://voideditor.com)
- 👋 [Discord](https://discord.gg/RSNjgaugJs)
- 🚙 [Roadmap](https://github.com/orgs/voideditor/projects/2)
- 🚙 [Project Board](https://github.com/orgs/voideditor/projects/2)
- 🔨 [Contribute](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md)
## Contributing
1. To get started working on Void, check out our Project Board! You can also see [HOW_TO_CONTRIBUTE](https://github.com/voideditor/void/blob/main/HOW_TO_CONTRIBUTE.md).
2. Feel free to attend a casual weekly meeting in our Discord channel!
## Reference
Void is a fork of the [vscode](https://github.com/microsoft/vscode) repository. For a guide to the 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
You can always reach us in our Discord server 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.
+5 -2
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.
@@ -134,7 +137,7 @@ If you want to know how our build pipeline works, see our build repo [here](http
For additional references, the Void team put together this list of links to get up and running with VSCode.
<details>
#### Links for Beginners
+3 -1
View File
@@ -842,7 +842,9 @@ export default tseslint.config(
'@xterm/xterm',
'yauzl',
'yazl',
'zlib'
'zlib',
// Void added this
'@modelcontextprotocol/sdk/**'
]
},
{
+68 -23
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.1",
"@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",
@@ -299,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": {
@@ -448,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": {
@@ -479,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": {
@@ -1817,11 +1820,17 @@
"integrity": "sha512-MDWhGtE+eHw5JW7lq4qhc5yRLS11ERl1c7Z6Xd0a58DozHES6EnNNwUWbMiG4J9Cgj053Bhk8zvlhFYKVhULwg==",
"license": "MIT"
},
"node_modules/@google/generative-ai": {
"version": "0.24.1",
"resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz",
"integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==",
"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"
}
@@ -2607,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",
@@ -4251,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",
@@ -23767,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",
+2 -2
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.1",
"@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",
+2 -1
View File
@@ -1,7 +1,8 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.3.2",
"voidVersion": "1.4.9",
"voidRelease": "0044",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
+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);
}
@@ -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';
@@ -37,6 +37,8 @@ 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
@@ -181,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>;
} | {
@@ -323,6 +326,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
@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
@@ -445,7 +449,8 @@ 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 })
}
}
@@ -532,19 +537,23 @@ 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
@@ -553,12 +562,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
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 })
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') {
@@ -581,8 +591,9 @@ class ChatThreadService extends Disposable implements IChatThreadService {
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)}`
}
@@ -595,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 }
}
@@ -636,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)
@@ -648,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
}
@@ -663,21 +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 {}
}
// 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 {}
};
@@ -712,7 +757,7 @@ 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 })
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 })
@@ -749,8 +794,13 @@ class ChatThreadService extends Disposable implements IChatThreadService {
shouldRetryLLM = false
nAttempts += 1
let resMessageIsDonePromise: (res: { type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }) => void // resolves when user approves this tool use (or if tool doesn't require approval)
const messageIsDonePromise = new Promise<{ type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }>((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 })
const llmCancelToken = this._llmMessageService.sendLLMMessage({
messagesType: 'chatMessages',
@@ -765,9 +815,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
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 })
resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls
resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls
},
onError: async (error) => {
resMessageIsDonePromise({ type: 'llmError', error: error })
@@ -818,7 +866,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
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 })
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 })
@@ -826,15 +874,19 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
}
// llm res success
const { toolCall } = llmRes
this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity
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
@@ -1294,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)
}
}
@@ -7,7 +7,7 @@ import { IWorkspaceContextService } from '../../../../platform/workspace/common/
import { IEditorService } from '../../../services/editor/common/editorService.js';
import { ChatMessage } from '../common/chatThreadServiceTypes.js';
import { getIsReasoningEnabledState, getReservedOutputTokenSpace, getModelCapabilities } from '../common/modelCapabilities.js';
import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.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, ProviderName } from '../common/voidSettingsTypes.js';
@@ -16,7 +16,10 @@ 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)'
@@ -36,7 +39,6 @@ type SimpleLLMMessage = {
}
const EMPTY_MESSAGE = '(empty message)'
const CHARS_PER_TOKEN = 4 // assume abysmal chars per token
const TRIM_TO_LEN = 120
@@ -237,18 +239,6 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop
}
export type GeminiMessage = {
role: 'user' | 'model'; // Gemini uses 'user' and 'model' roles
parts: (
| { text: string; }
| { functionCall: { tool_call: any } }
| { functionResponse: { name: ToolName, response: { result: string } } }
)[];
};
// --- CHAT ---
const prepareOpenAIOrAnthropicMessages = ({
@@ -417,14 +407,25 @@ const prepareOpenAIOrAnthropicMessages = ({
// ================ 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
}
@@ -456,8 +457,8 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
return { text: c.text }
}
else if (c.type === 'tool_use') {
latestToolName = c.name as ToolName
return { functionCall: { name: c.name as ToolName, args: c.input } }
latestToolName = c.name
return { functionCall: { id: c.id, name: c.name, args: c.input } }
}
else return null
}).filter(m => !!m)
@@ -475,7 +476,7 @@ const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => {
}
else if (c.type === 'tool_result') {
if (!latestToolName) return null
return { functionResponse: { name: latestToolName, response: { result: c.content } } }
return { functionResponse: { id: c.tool_use_id, name: latestToolName, response: { output: c.content } } }
}
else return null
}).filter(m => !!m)
@@ -539,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()
}
@@ -588,8 +590,10 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
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
}
@@ -674,13 +678,15 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess
contextWindow,
supportsSystemMessage,
} = getModelCapabilities(providerName, modelName, overridesOfModel)
const systemMessage = await this._generateChatMessagesSystemMessage(chatMode, specialToolFormat)
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, overridesOfModel)
const reservedOutputTokenSpace = getReservedOutputTokenSpace(providerName, modelName, { isReasoningEnabled, overridesOfModel })
const llmMessages = this._chatMessagesToSimpleMessages(chatMessages)
@@ -1179,6 +1179,11 @@ 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' })
}
}
@@ -1218,6 +1223,11 @@ 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: true })
@@ -1448,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
@@ -1732,6 +1747,11 @@ 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; }) => {
@@ -3,6 +3,7 @@
* 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';
@@ -29,6 +30,7 @@ export const IExtensionTransferService = createDecorator<IExtensionTransferServi
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',
@@ -40,6 +42,9 @@ const extensionBlacklist = [
];
const isBlacklisted = (fsPath: string | undefined) => {
return extensionBlacklist.find(bItem => fsPath?.includes(bItem))
}
class ExtensionTransferService extends Disposable implements IExtensionTransferService {
_serviceBrand: undefined;
@@ -87,15 +92,31 @@ class ExtensionTransferService extends Disposable implements IExtensionTransferS
await fileService.createFolder(toParent)
}
for (const extensionFolder of stat.children ?? []) {
if (extensionBlacklist.find(bItem => extensionFolder.resource.path.includes(bItem))) {
console.log('Skipping...', extensionFolder.resource.path)
continue
}
const from = extensionFolder.resource
const to = URI.joinPath(toParent, extensionFolder.name)
await fileService.copy(from, to, true)
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')
}
}
}
}
// Ensure the destination directory exists
} else {
console.log(`Skipping file that doesn't exist: ${from.toString()}`)
}
@@ -113,19 +134,38 @@ class ExtensionTransferService extends Disposable implements IExtensionTransferS
}
async deleteBlacklistExtensions(os: 'mac' | 'windows' | 'linux' | null) {
const fileService = this._fileService
const extensionsURI = getExtensionsFolder(os)
if (!extensionsURI) return
const eURI = await this._fileService.resolve(extensionsURI)
const eURI = await fileService.resolve(extensionsURI)
for (const child of eURI.children ?? []) {
// if is not blacklisted, continue
if (!extensionBlacklist.find(bItem => child.resource.path.includes(bItem))) {
continue
}
try {
console.log('Deleting extension', child.resource.fsPath)
await this._fileService.del(child.resource, { recursive: true, useTrash: true })
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)
@@ -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)
@@ -5,17 +5,19 @@
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 { 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,
@IExtensionTransferService private readonly extensionTransferService: IExtensionTransferService,
@IStorageService private readonly storageService: IStorageService,
) {
super();
this.initialize();
@@ -23,13 +25,23 @@ export class MiscWorkbenchContribs extends Disposable implements IWorkbenchContr
private initialize(): void {
// // delete blacklisted extensions once (this is for people who already installed them)
// const deleteExtensionsStorageId = 'void-deleted-blacklist'
// const alreadyDeleted = this.storageService.get(deleteExtensionsStorageId, StorageScope.APPLICATION)
// if (!alreadyDeleted) {
// this.storageService.store(deleteExtensionsStorageId, 'true', StorageScope.APPLICATION, StorageTarget.MACHINE)
// this.extensionTransferService.deleteBlacklistExtensions(os)
// }
// 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'))
})
}
}
@@ -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'),
}
});
}
@@ -239,27 +239,104 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId:
}
export const ApplyButtonsHTML = ({
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 settingsState = useSettingsState()
const isDisabled = !!isFeatureNameDisabled('Apply', settingsState) || !applyBoxId
const { currStreamStateRef, setApplying } = useApplyStreamState({ applyBoxId })
const onClickSubmit = useCallback(async () => {
if (currStreamStateRef.current === 'streaming') return
@@ -271,17 +348,22 @@ export const ApplyButtonsHTML = ({
uri: uri,
startBehavior: 'reject-conflicts',
}) ?? []
console.log('setting!!!', newApplyingUri)
setApplying(newApplyingUri)
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}.`)
})
metricsService.capture('Apply Code', { length: codeStr.length }) // capture the length only
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService])
}, [setApplying, currStreamStateRef, editCodeService, codeStr, uri, applyBoxId, metricsService, notificationService])
const onClickStop = useCallback(() => {
@@ -303,10 +385,7 @@ export const ApplyButtonsHTML = ({
if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri: uri, behavior: 'reject', removeCtrlKs: false })
}, [uri, applyBoxId, editCodeService])
const currStreamState = currStreamStateRef.current
console.log('currStreamState...', currStreamState)
if (currStreamState === 'streaming') {
return <IconShell1
Icon={Square}
@@ -314,12 +393,9 @@ export const ApplyButtonsHTML = ({
{...tooltipPropsForApplyBlock({ tooltipName: 'Stop' })}
/>
}
if (isDisabled) {
return null
}
if (currStreamState === 'idle-no-changes') {
return <IconShell1
Icon={Play}
@@ -327,7 +403,6 @@ export const ApplyButtonsHTML = ({
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
if (currStreamState === 'idle-has-changes') {
return <Fragment>
<IconShell1
@@ -348,6 +423,27 @@ export const ApplyButtonsHTML = ({
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,
@@ -451,7 +547,7 @@ export const BlockCodeApplyWrapper = ({
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} />
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={codeStr} language={language} />
</div>
</div>
@@ -14,7 +14,7 @@ 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 { voidOpenFileFn } from '../sidebar-tsx/SidebarChat.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,23 +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 || !link.selection) return;
if (!link) return;
// Use the updated voidOpenFileFn to open the file and handle selection
voidOpenFileFn(link.uri, accessor, [link.selection.startLineNumber, link.selection.endLineNumber]);
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={displayText}
onClick={onClick}
className={link ? 'underline hover:brightness-90 transition-all duration-200 cursor-pointer' : ''}
tooltip={tooltip || undefined}
/>
}
@@ -13,7 +13,7 @@ import { ChatMarkdownRender, ChatMessageLocation, getApplyBoxId } from '../markd
import { URI } from '../../../../../../../base/common/uri.js';
import { IDisposable } from '../../../../../../../base/common/lifecycle.js';
import { ErrorDisplay } from './ErrorDisplay.js';
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch } from '../util/inputs.js';
import { BlockCode, TextAreaFns, VoidCustomDropdownBox, VoidInputBox2, VoidSlider, VoidSwitch, VoidDiffEditor } from '../util/inputs.js';
import { ModelDropdown, } from '../void-settings-tsx/ModelDropdown.js';
import { PastThreadsList } from './SidebarThreadSelector.js';
import { VOID_CTRL_L_ACTION_ID } from '../../../actionIDs.js';
@@ -24,16 +24,18 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js';
import { getModelCapabilities, getIsReasoningEnabledState } from '../../../../common/modelCapabilities.js';
import { AlertTriangle, File, Ban, Check, ChevronRight, Dot, FileIcon, Pencil, Undo, Undo2, X, Flag, Copy as CopyIcon, Info, CirclePlus, Ellipsis, CircleEllipsis, Folder, ALargeSmall, TypeOutline, Text } from 'lucide-react';
import { ChatMessage, CheckpointEntry, StagingSelectionItem, ToolMessage } from '../../../../common/chatThreadServiceTypes.js';
import { approvalTypeOfToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes, ToolCallParams } from '../../../../common/toolsServiceTypes.js';
import { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, ToolName, LintErrorItem, ToolApprovalType, toolApprovalTypes } from '../../../../common/toolsServiceTypes.js';
import { CopyButton, EditToolAcceptRejectButtonsHTML, IconShell1, JumpToFileButton, JumpToTerminalButton, StatusIndicator, StatusIndicatorForApplyButton, useApplyStreamState, useEditToolStreamState } from '../markdown/ApplyBlockHoverButtons.js';
import { IsRunningType } from '../../../chatThreadService.js';
import { acceptAllBg, acceptBorder, buttonFontSize, buttonTextColor, rejectAllBg, rejectBg, rejectBorder } from '../../../../common/helpers/colors.js';
import { MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME, ToolName, toolNames } from '../../../../common/prompt/prompts.js';
import { builtinToolNames, isABuiltinToolName, MAX_FILE_CHARS_PAGE, MAX_TERMINAL_INACTIVE_TIME } from '../../../../common/prompt/prompts.js';
import { RawToolCallObj } from '../../../../common/sendLLMMessageTypes.js';
import ErrorBoundary from './ErrorBoundary.js';
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
import { removeMCPToolNamePrefix } from '../../../../common/mcpServiceTypes.js';
export const IconX = ({ size, className = '', ...props }: { size: number, className?: string } & React.SVGProps<SVGSVGElement>) => {
@@ -495,12 +497,12 @@ const ScrollToBottomContainer = ({ children, className, style, scrollContainerRe
);
};
const getRelative = (uri: URI, accessor: ReturnType<typeof useAccessor>) => {
export const getRelative = (uri: URI, accessor: ReturnType<typeof useAccessor>) => {
const workspaceContextService = accessor.get('IWorkspaceContextService')
let path: string
const isInside = workspaceContextService.isInsideWorkspace(uri)
if (isInside) {
const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath))
const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath?.startsWith(f.uri.fsPath))
if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') }
else { path = uri.fsPath }
}
@@ -662,81 +664,89 @@ export const SelectedFiles = (
key={thisKey}
className={`flex flex-col space-y-[1px]`}
>
{/* summarybox */}
<div
className={`
flex items-center gap-1 relative
px-1
w-fit h-fit
select-none
text-xs text-nowrap
border rounded-sm
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-1 hover:brightness-95 text-void-fg-1'}
${isThisSelectionProspective
? 'border-void-border-2'
: 'border-void-border-1'
}
hover:border-void-border-1
transition-all duration-150
`}
onClick={() => {
if (type !== 'staging') return; // (never)
if (isThisSelectionProspective) { // add prospective selection to selections
setSelections([...selections, selection])
}
else if (selection.type === 'File') { // open files
voidOpenFileFn(selection.uri, accessor);
const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile
if (wasAddedAsCurrentFile) {
// make it so the file is added permanently, not just as the current file
const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } }
setSelections([
...selections.slice(0, i),
newSelection,
...selections.slice(i + 1)
])
}
}
else if (selection.type === 'CodeSelection') {
voidOpenFileFn(selection.uri, accessor, selection.range);
}
else if (selection.type === 'Folder') {
// TODO!!! reveal in tree
}
}}
{/* tooltip for file path */}
<span className="truncate overflow-hidden text-ellipsis"
data-tooltip-id='void-tooltip'
data-tooltip-content={getRelative(selection.uri, accessor)}
data-tooltip-place='top'
data-tooltip-delay-show={3000}
>
{<SelectionIcon size={10} />}
{/* summarybox */}
<div
className={`
flex items-center gap-1 relative
px-1
w-fit h-fit
select-none
text-xs text-nowrap
border rounded-sm
${isThisSelectionProspective ? 'bg-void-bg-1 text-void-fg-3 opacity-80' : 'bg-void-bg-1 hover:brightness-95 text-void-fg-1'}
${isThisSelectionProspective
? 'border-void-border-2'
: 'border-void-border-1'
}
hover:border-void-border-1
transition-all duration-150
`}
onClick={() => {
if (type !== 'staging') return; // (never)
if (isThisSelectionProspective) { // add prospective selection to selections
setSelections([...selections, selection])
}
else if (selection.type === 'File') { // open files
voidOpenFileFn(selection.uri, accessor);
{ // file name and range
getBasename(selection.uri.fsPath)
+ (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '')
}
const wasAddedAsCurrentFile = selection.state.wasAddedAsCurrentFile
if (wasAddedAsCurrentFile) {
// make it so the file is added permanently, not just as the current file
const newSelection: StagingSelectionItem = { ...selection, state: { ...selection.state, wasAddedAsCurrentFile: false } }
setSelections([
...selections.slice(0, i),
newSelection,
...selections.slice(i + 1)
])
}
}
else if (selection.type === 'CodeSelection') {
voidOpenFileFn(selection.uri, accessor, selection.range);
}
else if (selection.type === 'Folder') {
// TODO!!! reveal in tree
}
}}
>
{<SelectionIcon size={10} />}
{selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ?
<span className={`text-[8px] 'void-opacity-60 text-void-fg-4`}>
{`(Current File)`}
</span>
: null
}
{ // file name and range
getBasename(selection.uri.fsPath)
+ (selection.type === 'CodeSelection' ? ` (${selection.range[0]}-${selection.range[1]})` : '')
}
{type === 'staging' && !isThisSelectionProspective ? // X button
<div // box for making it easier to click
className='cursor-pointer z-1 self-stretch flex items-center justify-center'
onClick={(e) => {
e.stopPropagation(); // don't open/close selection
if (type !== 'staging') return;
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
}}
>
<IconX
className='stroke-[2]'
size={10}
/>
</div>
: <></>
}
</div>
{selection.type === 'File' && selection.state.wasAddedAsCurrentFile && messageIdx === undefined && currentURI?.fsPath === selection.uri.fsPath ?
<span className={`text-[8px] 'void-opacity-60 text-void-fg-4`}>
{`(Current File)`}
</span>
: null
}
{type === 'staging' && !isThisSelectionProspective ? // X button
<div // box for making it easier to click
className='cursor-pointer z-1 self-stretch flex items-center justify-center'
onClick={(e) => {
e.stopPropagation(); // don't open/close selection
if (type !== 'staging') return;
setSelections([...selections.slice(0, i), ...selections.slice(i + 1)])
}}
>
<IconX
className='stroke-[2]'
size={10}
/>
</div>
: <></>
}
</div>
</span>
</div>
})}
@@ -907,11 +917,14 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
const desc1OnClick = () => voidOpenFileFn(params.uri, accessor)
const componentParams: ToolHeaderParams = { title, desc1, desc1OnClick, desc1Info, isError, icon, isRejected, }
const editToolType = toolMessage.name === 'edit_file' ? 'diff' : 'rewrite'
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
code={content}
type={editToolType}
/>
</ToolChildrenWrapper>
// JumpToFileButton removed in favor of FileLinkText
@@ -936,6 +949,7 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<Res
<EditToolChildren
uri={params.uri}
code={content}
type={editToolType}
/>
</ToolChildrenWrapper>
@@ -1115,7 +1129,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
if (e.key === 'Escape') {
onCloseEdit()
}
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
onSubmit()
}
}
@@ -1388,7 +1402,7 @@ const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => {
</span>
}
const titleOfToolName = {
const titleOfBuiltinToolName = {
'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') },
'ls_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') },
'get_dir_tree': { done: 'Inspected folder tree', proposed: 'Inspect folder tree', running: loadingTitleWrapper('Inspecting folder tree') },
@@ -1406,21 +1420,42 @@ const titleOfToolName = {
'read_lint_errors': { done: `Read lint errors`, proposed: 'Read lint errors', running: loadingTitleWrapper('Reading lint errors') },
'search_in_file': { done: 'Searched in file', proposed: 'Search in file', running: loadingTitleWrapper('Searching in file') },
} as const satisfies Record<ToolName, { done: any, proposed: any, running: any }>
} as const satisfies Record<BuiltinToolName, { done: any, proposed: any, running: any }>
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type'>): React.ReactNode => {
const getTitle = (toolMessage: Pick<ChatMessage & { role: 'tool' }, 'name' | 'type' | 'mcpServerName'>): React.ReactNode => {
const t = toolMessage
if (!toolNames.includes(t.name as ToolName)) return t.name // good measure
const toolName = t.name as ToolName
if (t.type === 'success') return titleOfToolName[toolName].done
if (t.type === 'running_now') return titleOfToolName[toolName].running
return titleOfToolName[toolName].proposed
// non-built-in title
if (!builtinToolNames.includes(t.name as BuiltinToolName)) {
// descriptor of Running or Ran etc
const descriptor =
t.type === 'success' ? 'Called'
: t.type === 'running_now' ? 'Calling'
: t.type === 'tool_request' ? 'Call'
: t.type === 'rejected' ? 'Call'
: t.type === 'invalid_params' ? 'Call'
: t.type === 'tool_error' ? 'Call'
: 'Call'
const title = `${descriptor} ${toolMessage.mcpServerName || 'MCP'}`
if (t.type === 'running_now' || t.type === 'tool_request')
return loadingTitleWrapper(title)
return title
}
// built-in title
else {
const toolName = t.name as BuiltinToolName
if (t.type === 'success') return titleOfBuiltinToolName[toolName].done
if (t.type === 'running_now') return titleOfBuiltinToolName[toolName].running
return titleOfBuiltinToolName[toolName].proposed
}
}
const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
const toolNameToDesc = (toolName: BuiltinToolName, _toolParams: BuiltinToolCallParams[BuiltinToolName] | undefined, accessor: ReturnType<typeof useAccessor>): {
desc1: React.ReactNode,
desc1Info?: string,
} => {
@@ -1431,95 +1466,95 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
const x = {
'read_file': () => {
const toolParams = _toolParams as ToolCallParams['read_file']
const toolParams = _toolParams as BuiltinToolCallParams['read_file']
return {
desc1: getBasename(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
};
},
'ls_dir': () => {
const toolParams = _toolParams as ToolCallParams['ls_dir']
const toolParams = _toolParams as BuiltinToolCallParams['ls_dir']
return {
desc1: getFolderName(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
};
},
'search_pathnames_only': () => {
const toolParams = _toolParams as ToolCallParams['search_pathnames_only']
const toolParams = _toolParams as BuiltinToolCallParams['search_pathnames_only']
return {
desc1: `"${toolParams.query}"`,
}
},
'search_for_files': () => {
const toolParams = _toolParams as ToolCallParams['search_for_files']
const toolParams = _toolParams as BuiltinToolCallParams['search_for_files']
return {
desc1: `"${toolParams.query}"`,
}
},
'search_in_file': () => {
const toolParams = _toolParams as ToolCallParams['search_in_file'];
const toolParams = _toolParams as BuiltinToolCallParams['search_in_file'];
return {
desc1: `"${toolParams.query}"`,
desc1Info: getRelative(toolParams.uri, accessor),
};
},
'create_file_or_folder': () => {
const toolParams = _toolParams as ToolCallParams['create_file_or_folder']
const toolParams = _toolParams as BuiltinToolCallParams['create_file_or_folder']
return {
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'delete_file_or_folder': () => {
const toolParams = _toolParams as ToolCallParams['delete_file_or_folder']
const toolParams = _toolParams as BuiltinToolCallParams['delete_file_or_folder']
return {
desc1: toolParams.isFolder ? getFolderName(toolParams.uri.fsPath) ?? '/' : getBasename(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'rewrite_file': () => {
const toolParams = _toolParams as ToolCallParams['rewrite_file']
const toolParams = _toolParams as BuiltinToolCallParams['rewrite_file']
return {
desc1: getBasename(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'edit_file': () => {
const toolParams = _toolParams as ToolCallParams['edit_file']
const toolParams = _toolParams as BuiltinToolCallParams['edit_file']
return {
desc1: getBasename(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'run_command': () => {
const toolParams = _toolParams as ToolCallParams['run_command']
const toolParams = _toolParams as BuiltinToolCallParams['run_command']
return {
desc1: `"${toolParams.command}"`,
}
},
'run_persistent_command': () => {
const toolParams = _toolParams as ToolCallParams['run_persistent_command']
const toolParams = _toolParams as BuiltinToolCallParams['run_persistent_command']
return {
desc1: `"${toolParams.command}"`,
}
},
'open_persistent_terminal': () => {
const toolParams = _toolParams as ToolCallParams['open_persistent_terminal']
const toolParams = _toolParams as BuiltinToolCallParams['open_persistent_terminal']
return { desc1: '' }
},
'kill_persistent_terminal': () => {
const toolParams = _toolParams as ToolCallParams['kill_persistent_terminal']
const toolParams = _toolParams as BuiltinToolCallParams['kill_persistent_terminal']
return { desc1: toolParams.persistentTerminalId }
},
'get_dir_tree': () => {
const toolParams = _toolParams as ToolCallParams['get_dir_tree']
const toolParams = _toolParams as BuiltinToolCallParams['get_dir_tree']
return {
desc1: getFolderName(toolParams.uri.fsPath) ?? '/',
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'read_lint_errors': () => {
const toolParams = _toolParams as ToolCallParams['read_lint_errors']
const toolParams = _toolParams as BuiltinToolCallParams['read_lint_errors']
return {
desc1: getBasename(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
@@ -1590,9 +1625,9 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
</button>
)
const approvalType = approvalTypeOfToolName[toolName]
const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'MCP tools'
const approvalToggle = approvalType ? <div key={approvalType} className="flex items-center ml-2 gap-x-1">
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc='Auto-approve' />
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
</div> : null
return <div className="flex gap-2 mx-0.5 items-center">
@@ -1604,7 +1639,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => {
return <div className={`${className ? className : ''} cursor-default select-none`}>
<div className='px-2 min-w-full'>
<div className='px-2 min-w-full overflow-hidden'>
{children}
</div>
</div>
@@ -1633,12 +1668,18 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => {
const EditToolChildren = ({ uri, code, type }: { uri: URI | undefined, code: string, type: 'diff' | 'rewrite' }) => {
const content = type === 'diff' ?
<VoidDiffEditor uri={uri} searchReplaceBlocks={code} />
: <ChatMarkdownRender string={`\`\`\`\n${code}\n\`\`\``} codeURI={uri} chatMessageLocation={undefined} />
return <div className='!select-text cursor-auto'>
<SmallProseWrapper>
<ChatMarkdownRender string={code} codeURI={uri} chatMessageLocation={undefined} />
{content}
</SmallProseWrapper>
</div>
}
@@ -1651,8 +1692,8 @@ const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
}
const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => {
if (!children) return null;
const [isOpen, setIsOpen] = useState(false);
if (!children) return null;
return (
<div className="w-full px-2 mt-0.5">
<div
@@ -1689,9 +1730,9 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr, toolName, threadId }:
const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: string }) => {
const InvalidTool = ({ toolName, message, mcpServerName }: { toolName: ToolName, message: string, mcpServerName: string | undefined }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'invalid_params' })
const title = getTitle({ name: toolName, type: 'invalid_params', mcpServerName })
const desc1 = 'Invalid parameters'
const icon = null
const isError = true
@@ -1705,9 +1746,9 @@ const InvalidTool = ({ toolName, message }: { toolName: ToolName, message: strin
return <ToolHeaderWrapper {...componentParams} />
}
const CanceledTool = ({ toolName }: { toolName: ToolName }) => {
const CanceledTool = ({ toolName, mcpServerName }: { toolName: ToolName, mcpServerName: string | undefined }) => {
const accessor = useAccessor()
const title = getTitle({ name: toolName, type: 'rejected' })
const title = getTitle({ name: toolName, type: 'rejected', mcpServerName })
const desc1 = ''
const icon = null
const isRejected = true
@@ -1819,9 +1860,61 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({
</>
}
type WrapperProps<T extends ToolName> = { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }
const MCPToolWrapper = ({ toolMessage }: WrapperProps<string>) => {
const accessor = useAccessor()
const mcpService = accessor.get('IMCPService')
type ResultWrapper<T extends ToolName> = (props: { toolMessage: Exclude<ToolMessage<T>, { type: 'invalid_params' }>, messageIdx: number, threadId: string }) => React.ReactNode
const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>, } } = {
const title = getTitle(toolMessage)
const desc1 = removeMCPToolNamePrefix(toolMessage.name)
const icon = null
if (toolMessage.type === 'running_now') return null // do not show running
const isError = false
const isRejected = toolMessage.type === 'rejected'
const { rawParams, params } = toolMessage
const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected, }
const paramsStr = JSON.stringify(params, null, 2)
componentParams.desc2 = <CopyButton codeStr={paramsStr} toolTipName={`Copy inputs: ${paramsStr}`} />
componentParams.info = !toolMessage.mcpServerName ? 'MCP tool not found' : undefined
// Add copy inputs button in desc2
if (toolMessage.type === 'success' || toolMessage.type === 'tool_request') {
const { result } = toolMessage
const resultStr = result ? mcpService.stringifyResult(result) : 'null'
componentParams.children = <ToolChildrenWrapper>
<SmallProseWrapper>
<ChatMarkdownRender
string={`\`\`\`json\n${resultStr}\n\`\`\``}
chatMessageLocation={undefined}
isApplyEnabled={false}
isLinkDetectionEnabled={true}
/>
</SmallProseWrapper>
</ToolChildrenWrapper>
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
}
type ResultWrapper<T extends ToolName> = (props: WrapperProps<T>) => React.ReactNode
const builtinToolNameToComponent: { [T in BuiltinToolName]: { resultWrapper: ResultWrapper<T>, } } = {
'read_file': {
resultWrapper: ({ toolMessage }) => {
const accessor = useAccessor()
@@ -1905,7 +1998,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</SmallProseWrapper>
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@@ -1960,7 +2053,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
}
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@@ -2009,7 +2102,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@@ -2064,7 +2157,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
@@ -2257,12 +2350,12 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
},
'rewrite_file': {
resultWrapper: (params) => {
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.newContent}${'\n```'}`} />
return <EditTool {...params} content={params.toolMessage.params.newContent} />
}
},
'edit_file': {
resultWrapper: (params) => {
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.searchReplaceBlocks}${'\n```'}`} />
return <EditTool {...params} content={params.toolMessage.params.searchReplaceBlocks} />
}
},
@@ -2442,11 +2535,15 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
if (chatMessage.type === 'invalid_params') {
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} />
<InvalidTool toolName={chatMessage.name} message={chatMessage.content} mcpServerName={chatMessage.mcpServerName} />
</div>
}
const ToolResultWrapper = toolNameToComponent[chatMessage.name]?.resultWrapper as ResultWrapper<ToolName>
const toolName = chatMessage.name
const isBuiltInTool = isABuiltinToolName(toolName)
const ToolResultWrapper = isBuiltInTool ? builtinToolNameToComponent[toolName]?.resultWrapper as ResultWrapper<ToolName>
: MCPToolWrapper as ResultWrapper<ToolName>
if (ToolResultWrapper)
return <>
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
@@ -2466,7 +2563,7 @@ const _ChatBubble = ({ threadId, chatMessage, currCheckpointIdx, isCommitted, me
else if (role === 'interrupted_streaming_tool') {
return <div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<CanceledTool toolName={chatMessage.name} />
<CanceledTool toolName={chatMessage.name} mcpServerName={chatMessage.mcpServerName} />
</div>
}
@@ -2605,7 +2702,7 @@ const CommandBarInChat = () => {
// !select-text cursor-auto
const fileDetailsContent = <div className="px-2 gap-1 w-full">
const fileDetailsContent = <div className="px-2 gap-1 w-full overflow-y-auto">
{sortedCommandBarURIs.map((uri, i) => {
const basename = getBasename(uri.fsPath)
@@ -2746,12 +2843,13 @@ const CommandBarInChat = () => {
const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => {
if (!isABuiltinToolName(toolCallSoFar.name)) return null
const accessor = useAccessor()
const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined
const title = titleOfToolName[toolCallSoFar.name].proposed
const title = titleOfBuiltinToolName[toolCallSoFar.name].proposed
const uriDone = toolCallSoFar.doneParams.includes('uri')
const desc1 = <span className='flex items-center'>
@@ -2772,12 +2870,11 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
<EditToolChildren
uri={uri}
code={toolCallSoFar.rawParams.search_replace_blocks ?? toolCallSoFar.rawParams.new_content ?? ''}
type={'rewrite'} // as it streams, show in rewrite format, don't make a diff editor
/>
<IconLoading />
</ToolHeaderWrapper>
}
@@ -2862,6 +2959,7 @@ export const SidebarChat = () => {
textAreaRef: textAreaRef,
scrollToBottom: () => scrollToBottom(scrollContainerRef),
})
}, [chatThreadsState, threadId, textAreaRef, scrollContainerRef, isResolved])
@@ -2958,7 +3056,7 @@ export const SidebarChat = () => {
setInstructionsAreEmpty(!newStr)
}, [setInstructionsAreEmpty])
const onKeyDown = useCallback((e: KeyboardEvent<HTMLTextAreaElement>) => {
if (e.key === 'Enter' && !e.shiftKey) {
if (e.key === 'Enter' && !e.shiftKey && !e.nativeEvent.isComposing) {
onSubmit()
} else if (e.key === 'Escape' && isRunning) {
onAbort()
@@ -20,6 +20,11 @@ import { URI } from '../../../../../../../base/common/uri.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
@@ -132,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;
@@ -951,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,
@@ -991,8 +996,7 @@ export const VoidInputBox = ({ onChangeText, onCreateInstance, inputBoxRef, plac
inputBoxRef.current = instance;
return disposables
}, [onChangeText, onCreateInstance, inputBoxRef])
}
}, [onChangeText, onCreateInstance, inputBoxRef])}
/>
};
@@ -1841,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,8 +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 { 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'
@@ -51,6 +51,9 @@ import { IConvertToLLMMessageService } from '../../../convertToLLMMessageService
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
@@ -78,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!
@@ -95,9 +100,10 @@ export const _registerServices = (accessor: ServicesAccessor) => {
editCodeService: accessor.get(IEditCodeService),
voidCommandBarService: accessor.get(IVoidCommandBarService),
modelService: accessor.get(IModelService),
mcpService: accessor.get(IMCPService),
}
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService } = stateServices
const { settingsStateService, chatThreadsStateService, refreshModelService, themeService, editCodeService, voidCommandBarService, modelService, mcpService } = stateServices
@@ -164,6 +170,11 @@ export const _registerServices = (accessor: ServicesAccessor) => {
})
)
disposables.push(
mcpService.onDidChangeState(() => {
mcpListeners.forEach(l => l())
})
)
return disposables
@@ -215,6 +226,9 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
ITerminalService: accessor.get(ITerminalService),
IExtensionManagementService: accessor.get(IExtensionManagementService),
IExtensionTransferService: accessor.get(IExtensionTransferService),
IMCPService: accessor.get(IMCPService),
IStorageService: accessor.get(IStorageService),
} as const
return reactAccessor
@@ -376,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
}
@@ -100,7 +100,7 @@ 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', 'openAICompatible'];
const cloudProviders: ProviderName[] = ['googleVertex', 'liteLLM', 'microsoftAzure', 'awsBedrock', 'openAICompatible'];
// Data structures for provider tabs
const providerNamesOfTab: Record<TabName, ProviderName[]> = {
@@ -123,6 +123,7 @@ const featureNameMap: { display: string, featureName: FeatureName }[] = [
{ 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 }) => {
File diff suppressed because it is too large Load Diff
@@ -50,6 +50,8 @@ export const VoidTooltip = () => {
padding: 0px 8px;
border-radius: 6px;
z-index: 999999;
max-width: 300px;
word-wrap: break-word;
}
#void-tooltip {
@@ -89,10 +89,7 @@ registerAction2(class extends Action2 {
});
}
async run(accessor: ServicesAccessor): Promise<void> {
// Get the views service to check if the sidebar is open
// const viewsService = accessor.get(IViewsService)
// Get services
const commandService = accessor.get(ICommandService)
const viewsService = accessor.get(IViewsService)
const metricsService = accessor.get(IMetricsService)
@@ -101,40 +98,47 @@ registerAction2(class extends Action2 {
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)
return
}
// if was already open
const model = accessor.get(ICodeEditorService).getActiveCodeEditor()?.getModel()
if (!model) return
const editor = editorService.getActiveCodeEditor()
const selectionRange = roundRangeToLines(editor?.getSelection(), { emptySelectionBehavior: 'null' })
// if has no selection, close + return
if (!selectionRange) {
viewsService.closeViewContainer(VOID_VIEW_CONTAINER_ID);
return;
// 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 },
})
}
// if has selection, add it
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 },
})
await chatThreadService.focusCurrentChat()
}
})
@@ -168,7 +172,7 @@ registerAction2(class extends Action2 {
const oldUI = await oldThread?.state.mountedInfo?.whenMounted
const oldSelns = oldThread?.state.stagingSelections
const oldVal = oldUI?.textAreaRef.current?.value
const oldVal = oldUI?.textAreaRef?.current?.value
// open and focus new thread
chatThreadsService.openNewThread()
@@ -22,7 +22,12 @@ 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
@@ -277,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 () => {
@@ -290,11 +297,12 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
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.`)
// 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 };
@@ -339,20 +347,20 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ
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)
}
if (!isPersistent) {
interrupt()
}
if (!resolveReason) throw new Error('Unexpected internal error: Promise.any should have resolved with a reason.')
// 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)
}
if (!isPersistent) result = `$ ${command}\n${result}`
result = removeAnsiEscapeCodes(result)
// trim
@@ -8,7 +8,7 @@ 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'
@@ -16,20 +16,15 @@ import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree
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) => {
@@ -47,8 +42,29 @@ const validateStr = (argName: string, value: unknown) => {
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) => {
@@ -110,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');
@@ -121,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,
@@ -138,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 = {
@@ -430,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 }) => {
@@ -446,7 +461,6 @@ export class ToolsService implements IToolsService {
await this.terminalToolService.killPersistentTerminal(persistentTerminalId)
return { result: {} }
},
}
@@ -550,7 +564,6 @@ export class ToolsService implements IToolsService {
kill_persistent_terminal: (params, _result) => {
return `Successfully closed terminal "${params.persistentTerminalId}".`;
},
}
@@ -58,6 +58,12 @@ 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
@@ -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
}
@@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta
import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js';
import { IFileService, IFileStat } from '../../../../platform/files/common/files.js';
import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js';
import { ShallowDirectoryItem, ToolCallParams, ToolResultType } from './toolsServiceTypes.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';
@@ -76,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 };
@@ -107,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`;
}
@@ -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()
@@ -60,6 +60,12 @@ export const defaultProviderSettings = {
apiKey: '',
azureApiVersion: '2024-05-01-preview',
},
awsBedrock: {
apiKey: '',
region: 'us-east-1', // add region setting
endpoint: '', // optionally allow overriding default
},
} as const
@@ -78,14 +84,19 @@ export const defaultModelsOfProvider = {
// 'gpt-4o-mini',
],
anthropic: [ // https://docs.anthropic.com/en/docs/about-claude/models
'claude-opus-4-0',
'claude-sonnet-4-0',
'claude-3-7-sonnet-latest',
'claude-3-5-sonnet-latest',
'claude-3-5-haiku-latest',
'claude-3-opus-latest',
],
xAI: [ // https://docs.x.ai/docs/models?cluster=us-east-1
'grok-2-latest',
'grok-3-latest',
'grok-2',
'grok-3',
'grok-3-mini',
'grok-3-fast',
'grok-3-mini-fast'
],
gemini: [ // https://ai.google.dev/gemini-api/docs/models/gemini
'gemini-2.5-pro-exp-03-25',
@@ -106,11 +117,14 @@ export const defaultModelsOfProvider = {
openRouter: [ // https://openrouter.ai/models
// 'anthropic/claude-3.7-sonnet:thinking',
'anthropic/claude-opus-4',
'anthropic/claude-sonnet-4',
'qwen/qwen3-235b-a22b',
'anthropic/claude-3.7-sonnet',
'anthropic/claude-3.5-sonnet',
'deepseek/deepseek-r1',
'deepseek/deepseek-r1-zero:free',
'mistralai/devstral-small:free'
// 'openrouter/quasar-alpha',
// 'google/gemini-2.5-pro-preview-03-25',
// 'mistralai/codestral-2501',
@@ -128,13 +142,16 @@ export const defaultModelsOfProvider = {
],
mistral: [ // https://docs.mistral.ai/getting-started/models/models_overview/
'codestral-latest',
'devstral-small-latest',
'mistral-large-latest',
'mistral-medium-latest',
'ministral-3b-latest',
'ministral-8b-latest',
],
openAICompatible: [], // fallback
googleVertex: [],
microsoftAzure: [],
awsBedrock: [],
liteLLM: [],
@@ -153,6 +170,8 @@ export type VoidStaticModelInfo = { // not stateful
specialToolFormat?: 'openai-style' | 'anthropic-style' | 'gemini-style', // typically you should use 'openai-style'. null means "can't call tools by default", and asks the LLM to output XML in agent mode
supportsFIM: boolean; // whether the model was specifically designed for autocomplete or "FIM" ("fill-in-middle" format)
additionalOpenAIPayload?: { [key: string]: string } // additional payload in the message body for requests that are openai-compatible (ollama, vllm, openai, openrouter, etc)
// reasoning options
reasoningCapabilities: false | {
readonly supportsReasoning: true; // for clarity, this must be true if anything below is specified
@@ -185,8 +204,20 @@ export type VoidStaticModelInfo = { // not stateful
// if you change the above type, remember to update the Settings link
export type ModelOverrides = Pick<VoidStaticModelInfo,
'contextWindow' | 'reservedOutputTokenSpace' | 'specialToolFormat' | 'supportsSystemMessage' | 'supportsFIM' | 'reasoningCapabilities'
export const modelOverrideKeys = [
'contextWindow',
'reservedOutputTokenSpace',
'supportsSystemMessage',
'specialToolFormat',
'supportsFIM',
'reasoningCapabilities',
'additionalOpenAIPayload'
] as const
export type ModelOverrides = Pick<
VoidStaticModelInfo,
(typeof modelOverrideKeys)[number]
>
@@ -248,6 +279,12 @@ const openSourceModelOptions_assumingOAICompat = {
reasoningCapabilities: false,
contextWindow: 32_000, reservedOutputTokenSpace: 4_096,
},
'devstral': {
supportsFIM: false,
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
contextWindow: 131_000, reservedOutputTokenSpace: 8_192,
},
'openhands-lm-32b': { // https://www.all-hands.dev/blog/introducing-openhands-lm-32b----a-strong-open-coding-agent-model
supportsFIM: false,
supportsSystemMessage: 'system-role',
@@ -362,22 +399,28 @@ const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallbac
: VoidStaticModelInfo & { modelName: string, recognizedModelName: string } => {
const opts = obj[recognizedModelName]
const supportsSystemMessage = opts.supportsSystemMessage === 'separated'
? 'system-role'
: opts.supportsSystemMessage
return {
recognizedModelName,
modelName,
...opts,
supportsSystemMessage: opts.supportsSystemMessage ? 'system-role' : false,
supportsSystemMessage: supportsSystemMessage,
cost: { input: 0, output: 0 },
downloadable: false,
...fallbackKnownValues
}
};
}
if (lower.includes('gemini') && (lower.includes('2.5') || lower.includes('2-5'))) return toFallback(geminiModelOptions, 'gemini-2.5-pro-exp-03-25')
if (lower.includes('claude-3-5') || lower.includes('claude-3.5')) return toFallback(anthropicModelOptions, 'claude-3-5-sonnet-20241022')
if (lower.includes('claude')) return toFallback(anthropicModelOptions, 'claude-3-7-sonnet-20250219')
if (lower.includes('grok')) return toFallback(xAIModelOptions, 'grok-2')
if (lower.includes('grok2') || lower.includes('grok2')) return toFallback(xAIModelOptions, 'grok-2')
if (lower.includes('grok')) return toFallback(xAIModelOptions, 'grok-3')
if (lower.includes('deepseek-r1') || lower.includes('deepseek-reasoner')) return toFallback(openSourceModelOptions_assumingOAICompat, 'deepseekR1')
if (lower.includes('deepseek') && lower.includes('v2')) return toFallback(openSourceModelOptions_assumingOAICompat, 'deepseekCoderV2')
@@ -397,6 +440,7 @@ const extensiveModelOptionsFallback: VoidStaticProviderInfo['modelOptionsFallbac
if (lower.includes('qwq')) { return toFallback(openSourceModelOptions_assumingOAICompat, 'qwq') }
if (lower.includes('phi4')) return toFallback(openSourceModelOptions_assumingOAICompat, 'phi4')
if (lower.includes('codestral')) return toFallback(openSourceModelOptions_assumingOAICompat, 'codestral')
if (lower.includes('devstral')) return toFallback(openSourceModelOptions_assumingOAICompat, 'devstral')
if (lower.includes('gemma')) return toFallback(openSourceModelOptions_assumingOAICompat, 'gemma')
@@ -445,7 +489,41 @@ const anthropicModelOptions = {
supportsReasoning: true,
canTurnOffReasoning: true,
canIOReasoning: true,
reasoningReservedOutputTokenSpace: 64_000, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy)
},
},
'claude-opus-4-20250514': {
contextWindow: 200_000,
reservedOutputTokenSpace: 8_192,
cost: { input: 15.00, cache_read: 1.50, cache_write: 18.75, output: 30.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: {
supportsReasoning: true,
canTurnOffReasoning: true,
canIOReasoning: true,
reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy)
},
},
'claude-sonnet-4-20250514': {
contextWindow: 200_000,
reservedOutputTokenSpace: 8_192,
cost: { input: 3.00, cache_read: 0.30, cache_write: 3.75, output: 6.00 },
downloadable: false,
supportsFIM: false,
specialToolFormat: 'anthropic-style',
supportsSystemMessage: 'separated',
reasoningCapabilities: {
supportsReasoning: true,
canTurnOffReasoning: true,
canIOReasoning: true,
reasoningReservedOutputTokenSpace: 8192, // can bump it to 128_000 with beta mode output-128k-2025-02-19
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000. we cap at 8192 because above is typically not necessary (often even buggy)
},
@@ -508,6 +586,10 @@ const anthropicSettings: VoidStaticProviderInfo = {
modelOptionsFallback: (modelName) => {
const lower = modelName.toLowerCase()
let fallbackName: keyof typeof anthropicModelOptions | null = null
if (lower.includes('claude-4-opus') || lower.includes('claude-opus-4')) fallbackName = 'claude-opus-4-20250514'
if (lower.includes('claude-4-sonnet') || lower.includes('claude-sonnet-4')) fallbackName = 'claude-sonnet-4-20250514'
if (lower.includes('claude-3-7-sonnet')) fallbackName = 'claude-3-7-sonnet-20250219'
if (lower.includes('claude-3-5-sonnet')) fallbackName = 'claude-3-5-sonnet-20241022'
if (lower.includes('claude-3-5-haiku')) fallbackName = 'claude-3-5-haiku-20241022'
@@ -621,6 +703,16 @@ const openAIModelOptions = { // https://platform.openai.com/docs/pricing
} as const satisfies { [s: string]: VoidStaticModelInfo }
// https://platform.openai.com/docs/guides/reasoning?api-mode=chat
const openAICompatIncludeInPayloadReasoning = (reasoningInfo: SendableReasoningInfo) => {
if (!reasoningInfo?.isReasoningEnabled) return null
if (reasoningInfo.type === 'effort_slider_value') {
return { reasoning_effort: reasoningInfo.reasoningEffort }
}
return null
}
const openAISettings: VoidStaticProviderInfo = {
modelOptions: openAIModelOptions,
modelOptionsFallback: (modelName) => {
@@ -633,17 +725,7 @@ const openAISettings: VoidStaticProviderInfo = {
return null
},
providerReasoningIOSettings: {
input: {
// https://platform.openai.com/docs/guides/reasoning?api-mode=chat
includeInPayload: (reasoningInfo) => {
if (!reasoningInfo?.isReasoningEnabled) return null
if (reasoningInfo.type === 'effort_slider_value') {
return { reasoning_effort: reasoningInfo.reasoningEffort }
}
return null
}
},
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
},
}
@@ -658,6 +740,7 @@ const xAIModelOptions = {
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
specialToolFormat: 'openai-style',
reasoningCapabilities: false,
},
'grok-3': {
@@ -667,6 +750,7 @@ const xAIModelOptions = {
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
specialToolFormat: 'openai-style',
reasoningCapabilities: false,
},
'grok-3-fast': {
@@ -676,6 +760,7 @@ const xAIModelOptions = {
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
specialToolFormat: 'openai-style',
reasoningCapabilities: false,
},
// only mini supports thinking
@@ -686,6 +771,7 @@ const xAIModelOptions = {
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
specialToolFormat: 'openai-style',
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'high'], default: 'low' } },
},
'grok-3-mini-fast': {
@@ -695,6 +781,7 @@ const xAIModelOptions = {
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
specialToolFormat: 'openai-style',
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: false, reasoningSlider: { type: 'effort_slider', values: ['low', 'high'], default: 'low' } },
},
} as const satisfies { [s: string]: VoidStaticModelInfo }
@@ -705,16 +792,21 @@ const xAISettings: VoidStaticProviderInfo = {
const lower = modelName.toLowerCase()
let fallbackName: keyof typeof xAIModelOptions | null = null
if (lower.includes('grok-2')) fallbackName = 'grok-2'
if (lower.includes('grok-3')) fallbackName = 'grok-3'
if (lower.includes('grok')) fallbackName = 'grok-3'
if (fallbackName) return { modelName: fallbackName, recognizedModelName: fallbackName, ...xAIModelOptions[fallbackName] }
return null
},
// same implementation as openai
providerReasoningIOSettings: openAISettings.providerReasoningIOSettings,
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
},
}
// ---------------- GEMINI ----------------
const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
// https://ai.google.dev/gemini-api/docs/thinking#set-budget
'gemini-2.5-pro-preview-05-06': {
contextWindow: 1_048_576,
reservedOutputTokenSpace: 8_192,
@@ -723,7 +815,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
supportsFIM: false,
supportsSystemMessage: 'separated',
specialToolFormat: 'gemini-style',
reasoningCapabilities: false,
reasoningCapabilities: {
supportsReasoning: true,
canTurnOffReasoning: true,
canIOReasoning: false,
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576
reasoningReservedOutputTokenSpace: 8192,
},
},
'gemini-2.0-flash-lite': {
contextWindow: 1_048_576,
@@ -733,7 +831,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
supportsFIM: false,
supportsSystemMessage: 'separated',
specialToolFormat: 'gemini-style',
reasoningCapabilities: false,
reasoningCapabilities: false, // no reasoning
},
'gemini-2.5-flash-preview-04-17': {
contextWindow: 1_048_576,
@@ -743,7 +841,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
supportsFIM: false,
supportsSystemMessage: 'separated',
specialToolFormat: 'gemini-style',
reasoningCapabilities: false,
reasoningCapabilities: {
supportsReasoning: true,
canTurnOffReasoning: true,
canIOReasoning: false,
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576
reasoningReservedOutputTokenSpace: 8192,
},
},
'gemini-2.5-pro-exp-03-25': {
contextWindow: 1_048_576,
@@ -753,7 +857,13 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
supportsFIM: false,
supportsSystemMessage: 'separated',
specialToolFormat: 'gemini-style',
reasoningCapabilities: false,
reasoningCapabilities: {
supportsReasoning: true,
canTurnOffReasoning: true,
canIOReasoning: false,
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // max is really 24576
reasoningReservedOutputTokenSpace: 8192,
},
},
'gemini-2.0-flash': {
contextWindow: 1_048_576,
@@ -809,7 +919,7 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing
const geminiSettings: VoidStaticProviderInfo = {
modelOptions: geminiModelOptions,
modelOptionsFallback: (modelName) => { return null }
modelOptionsFallback: (modelName) => { return null },
}
@@ -835,11 +945,12 @@ const deepseekModelOptions = {
const deepseekSettings: VoidStaticProviderInfo = {
modelOptions: deepseekModelOptions,
modelOptionsFallback: (modelName) => { return null },
providerReasoningIOSettings: {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://api-docs.deepseek.com/guides/reasoning_model
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
output: { nameOfFieldInDelta: 'reasoning_content' },
},
modelOptionsFallback: (modelName) => { return null }
}
@@ -856,6 +967,15 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'mistral-medium-latest': { // https://openrouter.ai/mistralai/mistral-medium-3
contextWindow: 131_000,
reservedOutputTokenSpace: 8_192,
cost: { input: 0.40, output: 2.00 },
supportsFIM: false,
downloadable: { sizeGb: 'not-known' },
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'codestral-latest': {
contextWindow: 256_000,
reservedOutputTokenSpace: 8_192,
@@ -865,6 +985,33 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'magistral-medium-latest': {
contextWindow: 256_000,
reservedOutputTokenSpace: 8_192,
cost: { input: 0.30, output: 0.90 }, // TODO: check this
supportsFIM: true,
downloadable: { sizeGb: 13 },
supportsSystemMessage: 'system-role',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
},
'magistral-small-latest': {
contextWindow: 40_000,
reservedOutputTokenSpace: 8_192,
cost: { input: 0.30, output: 0.90 }, // TODO: check this
supportsFIM: true,
downloadable: { sizeGb: 13 },
supportsSystemMessage: 'system-role',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: true, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
},
'devstral-small-latest': { //https://openrouter.ai/mistralai/devstral-small:free
contextWindow: 131_000,
reservedOutputTokenSpace: 8_192,
cost: { input: 0, output: 0 },
supportsFIM: false,
downloadable: { sizeGb: 14 }, //https://ollama.com/library/devstral
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'ministral-8b-latest': { // ollama 'mistral'
contextWindow: 131_000,
reservedOutputTokenSpace: 4_096,
@@ -888,6 +1035,9 @@ const mistralModelOptions = { // https://mistral.ai/products/la-plateforme#prici
const mistralSettings: VoidStaticProviderInfo = {
modelOptions: mistralModelOptions,
modelOptionsFallback: (modelName) => { return null },
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
},
}
@@ -931,11 +1081,13 @@ const groqModelOptions = { // https://console.groq.com/docs/models, https://groq
},
} as const satisfies { [s: string]: VoidStaticModelInfo }
const groqSettings: VoidStaticProviderInfo = {
modelOptions: groqModelOptions,
modelOptionsFallback: (modelName) => { return null },
providerReasoningIOSettings: {
// Must be set to either parsed or hidden when using tool calling https://console.groq.com/docs/reasoning
input: {
includeInPayload: (reasoningInfo) => {
if (!reasoningInfo?.isReasoningEnabled) return null
if (reasoningInfo.type === 'budget_slider_value') {
return { reasoning_format: 'parsed' }
}
@@ -943,9 +1095,7 @@ const groqSettings: VoidStaticProviderInfo = {
}
},
output: { nameOfFieldInDelta: 'reasoning' },
}, // Must be set to either parsed or hidden when using tool calling https://console.groq.com/docs/reasoning
modelOptions: groqModelOptions,
modelOptionsFallback: (modelName) => { return null }
},
}
@@ -954,7 +1104,10 @@ const googleVertexModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
const googleVertexSettings: VoidStaticProviderInfo = {
modelOptions: googleVertexModelOptions,
modelOptionsFallback: (modelName) => { return null }
modelOptionsFallback: (modelName) => { return null },
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
},
}
// ---------------- MICROSOFT AZURE ----------------
@@ -962,7 +1115,22 @@ const microsoftAzureModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
const microsoftAzureSettings: VoidStaticProviderInfo = {
modelOptions: microsoftAzureModelOptions,
modelOptionsFallback: (modelName) => { return null }
modelOptionsFallback: (modelName) => { return null },
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
},
}
// ---------------- AWS BEDROCK ----------------
const awsBedrockModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
const awsBedrockSettings: VoidStaticProviderInfo = {
modelOptions: awsBedrockModelOptions,
modelOptionsFallback: (modelName) => { return null },
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
},
}
@@ -1031,42 +1199,67 @@ const ollamaModelOptions = {
supportsSystemMessage: 'system-role',
reasoningCapabilities: { supportsReasoning: true, canIOReasoning: false, canTurnOffReasoning: false, openSourceThinkTags: ['<think>', '</think>'] },
},
'devstral:latest': {
contextWindow: 131_000,
reservedOutputTokenSpace: 8_192,
cost: { input: 0, output: 0 },
downloadable: { sizeGb: 14 },
supportsFIM: false,
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
} as const satisfies Record<string, VoidStaticModelInfo>
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1'] as const satisfies (keyof typeof ollamaModelOptions)[]
export const ollamaRecommendedModels = ['qwen2.5-coder:1.5b', 'llama3.1', 'qwq', 'deepseek-r1', 'devstral:latest'] as const satisfies (keyof typeof ollamaModelOptions)[]
const vLLMSettings: VoidStaticProviderInfo = {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' }, },
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
modelOptions: {}, // TODO
modelOptions: {},
providerReasoningIOSettings: {
// reasoning: OAICompat + response.choices[0].delta.reasoning_content // https://docs.vllm.ai/en/stable/features/reasoning_outputs.html#streaming-chat-completions
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
output: { nameOfFieldInDelta: 'reasoning_content' },
},
}
const lmStudioSettings: VoidStaticProviderInfo = {
providerReasoningIOSettings: { output: { needsManualParse: true }, },
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' }, contextWindow: 4_096 }),
modelOptions: {}, // TODO
modelOptions: {},
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
output: { needsManualParse: true },
},
}
const ollamaSettings: VoidStaticProviderInfo = {
// reasoning: we need to filter out reasoning <think> tags manually
providerReasoningIOSettings: { output: { needsManualParse: true }, },
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
modelOptions: ollamaModelOptions,
providerReasoningIOSettings: {
// reasoning: we need to filter out reasoning <think> tags manually
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
output: { needsManualParse: true },
},
}
const openaiCompatible: VoidStaticProviderInfo = {
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName),
modelOptions: {},
providerReasoningIOSettings: {
// reasoning: we have no idea what endpoint they used, so we can't consistently parse out reasoning
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
output: { nameOfFieldInDelta: 'reasoning_content' },
},
}
const liteLLMSettings: VoidStaticProviderInfo = { // https://docs.litellm.ai/docs/reasoning_content
providerReasoningIOSettings: { output: { nameOfFieldInDelta: 'reasoning_content' } },
modelOptionsFallback: (modelName) => extensiveModelOptionsFallback(modelName, { downloadable: { sizeGb: 'not-known' } }),
modelOptions: {}, // TODO
modelOptions: {},
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
output: { nameOfFieldInDelta: 'reasoning_content' },
},
}
@@ -1133,6 +1326,24 @@ const openRouterModelOptions_assumingOpenAICompat = {
cost: { input: 0.8, output: 2.4 },
downloadable: false,
},
'anthropic/claude-opus-4': {
contextWindow: 200_000,
reservedOutputTokenSpace: null,
cost: { input: 15.00, output: 75.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'anthropic/claude-sonnet-4': {
contextWindow: 200_000,
reservedOutputTokenSpace: null,
cost: { input: 15.00, output: 75.00 },
downloadable: false,
supportsFIM: false,
supportsSystemMessage: 'system-role',
reasoningCapabilities: false,
},
'anthropic/claude-3.7-sonnet:thinking': {
contextWindow: 200_000,
reservedOutputTokenSpace: null,
@@ -1144,7 +1355,7 @@ const openRouterModelOptions_assumingOpenAICompat = {
supportsReasoning: true,
canTurnOffReasoning: false,
canIOReasoning: true,
reasoningReservedOutputTokenSpace: 64_000,
reasoningReservedOutputTokenSpace: 8192,
reasoningSlider: { type: 'budget_slider', min: 1024, max: 8192, default: 1024 }, // they recommend batching if max > 32_000.
},
},
@@ -1174,6 +1385,14 @@ const openRouterModelOptions_assumingOpenAICompat = {
downloadable: false,
reasoningCapabilities: false,
},
'mistralai/devstral-small:free': {
...openSourceModelOptions_assumingOAICompat.devstral,
contextWindow: 130_000,
reservedOutputTokenSpace: null,
cost: { input: 0, output: 0 },
downloadable: false,
reasoningCapabilities: false,
},
'qwen/qwen-2.5-coder-32b-instruct': {
...openSourceModelOptions_assumingOAICompat['qwen2.5coder'],
contextWindow: 33_000,
@@ -1191,8 +1410,17 @@ const openRouterModelOptions_assumingOpenAICompat = {
} as const satisfies { [s: string]: VoidStaticModelInfo }
const openRouterSettings: VoidStaticProviderInfo = {
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
modelOptions: openRouterModelOptions_assumingOpenAICompat,
modelOptionsFallback: (modelName) => {
const res = extensiveModelOptionsFallback(modelName)
// openRouter does not support gemini-style, use openai-style instead
if (res?.specialToolFormat === 'gemini-style') {
res.specialToolFormat = 'openai-style'
}
return res
},
providerReasoningIOSettings: {
// reasoning: OAICompat + response.choices[0].delta.reasoning : payload should have {include_reasoning: true} https://openrouter.ai/announcements/reasoning-tokens-for-thinking-models
input: {
// https://openrouter.ai/docs/use-cases/reasoning-tokens
includeInPayload: (reasoningInfo) => {
@@ -1216,16 +1444,6 @@ const openRouterSettings: VoidStaticProviderInfo = {
},
output: { nameOfFieldInDelta: 'reasoning' },
},
modelOptions: openRouterModelOptions_assumingOpenAICompat,
// TODO!!! send a query to openrouter to get the price, etc.
modelOptionsFallback: (modelName) => {
const res = extensiveModelOptionsFallback(modelName)
// openRouter does not support gemini-style, use openai-style instead
if (res?.specialToolFormat === 'gemini-style') {
res.specialToolFormat = 'openai-style'
}
return res
},
}
@@ -1255,6 +1473,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
googleVertex: googleVertexSettings,
microsoftAzure: microsoftAzureSettings,
awsBedrock: awsBedrockSettings,
} as const
@@ -1347,8 +1566,7 @@ export const getSendableReasoningInfo = (
overridesOfModel: OverridesOfModel | undefined,
): SendableReasoningInfo => {
const { canIOReasoning, reasoningSlider: reasoningBudgetSlider } = getModelCapabilities(providerName, modelName, overridesOfModel).reasoningCapabilities || {}
if (!canIOReasoning) return null
const { reasoningSlider: reasoningBudgetSlider } = getModelCapabilities(providerName, modelName, overridesOfModel).reasoningCapabilities || {}
const isReasoningEnabled = getIsReasoningEnabledState(featureName, providerName, modelName, modelSelectionOptions, overridesOfModel)
if (!isReasoningEnabled) return null
@@ -9,7 +9,7 @@ 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 { approvalTypeOfBuiltinToolName, BuiltinToolCallParams, BuiltinToolName, BuiltinToolResultType, ToolName } from '../toolsServiceTypes.js';
import { ChatMode } from '../voidSettingsTypes.js';
// Triple backtick wrapper used throughout the prompts for code blocks
@@ -147,6 +147,8 @@ export type InternalToolInfo = {
params: {
[paramName: string]: { description: string }
},
// Only if the tool is from an MCP server
mcpServerName?: string,
}
@@ -181,191 +183,197 @@ export type SnakeCaseKeys<T extends Record<string, any>> = {
// 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 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,
},
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: `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.' },
},
},
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
}
@@ -382,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}>`
@@ -391,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 = (`\
@@ -417,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.`
@@ -451,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.`)
@@ -479,12 +489,13 @@ ${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 if known (otherwise omit).
- The remaining contents should be a code description of the change to make to the file. \
@@ -525,7 +536,9 @@ ${details.map((d, i) => `${i + 1}. ${d}`).join('\n\n')}`)
// chat_systemMessage({ chatMode, workspaceFolders: [], openedURIs: [], activeURI: 'pee', persistentTerminalIDs: [], directoryStr: 'lol', }))
// }
const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
export const DEFAULT_FILE_SIZE_LIMIT = 2_000_000
export const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: number): Promise<{
val: string,
truncated: boolean,
fullFileLen: number,
@@ -549,46 +562,83 @@ const readFile = async (fileService: IFileService, uri: URI, fileSizeLimit: numb
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 }
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[] = []
selnsStrs = await Promise.all(currSelns?.map(async (s) => {
const selnsStrs = await Promise.all(
(currSelns ?? []).map(async (s) =>
messageOfSelection(s, {
...opts,
folderOpts: { maxChildren: 100, maxCharsPerFile: 100_000, }
})
)
)
if (s.type === 'File' || s.type === 'CodeSelection') {
const { val } = await readFile(opts.fileService, s.uri, 2_000_000)
const lineNumAdd = s.type === 'CodeSelection' ? lineNumAddition(s.range) : ''
const content = val === null ? 'null' : `${tripleTick[0]}${s.language}\n${val}\n${tripleTick[1]}`
const str = `${s.uri.fsPath}${lineNumAdd}:\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: 100 })
const strOfFiles = await Promise.all(uris.map(async uri => {
const { val, truncated } = await readFile(opts.fileService, uri, 100_000)
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 ''
}) ?? [])
const selnsStr = selnsStrs.join('\n') ?? ''
let str = ''
str += `${instructions}`
const selnsStr = selnsStrs.join('\n\n') ?? ''
if (selnsStr) str += `\n---\nSELECTIONS\n${selnsStr}`
return str;
}
@@ -943,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,7 +3,8 @@
* Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information.
*--------------------------------------------------------------------------------------*/
import { ToolName, ToolParamName } from './prompt/prompts.js'
import { InternalToolInfo } from './prompt/prompts.js'
import { ToolName, ToolParamName } from './toolsServiceTypes.js'
import { ChatMode, ModelSelection, ModelSelectionOptions, OverridesOfModel, ProviderName, RefreshableProviderName, SettingsOfProvider } from './voidSettingsTypes.js'
@@ -56,13 +57,13 @@ export type GeminiLLMChatMessage = {
role: 'model'
parts: (
| { text: string; }
| { functionCall: { name: ToolName, args: object } }
| { functionCall: { id: string; name: ToolName, args: Record<string, unknown> } }
)[];
} | {
role: 'user';
parts: (
| { text: string; }
| { functionResponse: { name: ToolName, response: { result: string } } }
| { functionResponse: { id: string; name: ToolName, response: { output: string } } }
)[];
}
@@ -78,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;
};
@@ -133,6 +134,7 @@ export type SendLLMMessageParams = {
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,7 +18,7 @@ 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',
@@ -28,16 +30,19 @@ export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'term
}
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 },
@@ -58,7 +63,7 @@ export type ToolCallParams = {
}
// RESULT OF TOOL CALL
export type ToolResultType = {
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, },
@@ -78,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')
@@ -13,7 +13,7 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo
import { IMetricsService } from './metricsService.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, OverridesOfModel, defaultOverridesOfModel } 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
@@ -43,6 +43,7 @@ export type VoidSettingsState = {
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
}
@@ -62,6 +63,7 @@ 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>;
@@ -73,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>;
}
@@ -110,6 +116,7 @@ export const modelFilterOfFeatureName: {
'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, },
}
@@ -207,11 +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
}
@@ -255,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())
@@ -272,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()
@@ -361,6 +381,7 @@ 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,
@@ -368,6 +389,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
settingsOfProvider: newSettingsOfProvider,
globalSettings: newGlobalSettings,
overridesOfModel: newOverridesOfModel,
mcpUserStateOfName: newMCPUserStateOfName,
}
this.state = _validatedModelState(newState)
@@ -381,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) => {
@@ -398,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()
}
@@ -417,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()
}
}
@@ -531,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 });
}
}
@@ -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
}
@@ -103,6 +103,9 @@ export const displayInfoOfProviderName = (providerName: ProviderName): DisplayIn
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}"`)
}
@@ -120,6 +123,7 @@ export const subTextMdOfProviderName = (providerName: ProviderName): string => {
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 === '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).'
@@ -151,7 +155,8 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'mistral' ? 'api-key...' :
providerName === 'googleVertex' ? 'AIzaSy...' :
providerName === 'microsoftAzure' ? 'key-...' :
'',
providerName === 'awsBedrock' ? 'key-...' :
'',
isPasswordField: true,
}
@@ -165,14 +170,16 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
providerName === 'googleVertex' ? 'baseURL' :
providerName === 'microsoftAzure' ? 'baseURL' :
providerName === 'liteLLM' ? 'baseURL' :
'(never)',
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)',
}
@@ -185,7 +192,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
return {
title: 'Region',
placeholder: providerName === 'googleVertex' ? defaultProviderSettings.googleVertex.region
: ''
: providerName === 'awsBedrock'
? defaultProviderSettings.awsBedrock.region
: ''
}
}
else if (settingName === 'azureApiVersion') {
@@ -222,12 +231,9 @@ export const displayInfoOfSettingName = (providerName: ProviderName, settingName
}
throw new Error(`displayInfo: Unknown setting name: "${settingName}"`)
}
const defaultCustomSettings: Record<CustomSettingName, undefined> = {
apiKey: undefined,
endpoint: undefined,
@@ -340,6 +346,12 @@ export const defaultSettingsOfProvider: SettingsOfProvider = {
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.microsoftAzure),
_didFillInProviderSettings: undefined,
},
awsBedrock: { // aggregator (serves models from multiple providers)
...defaultCustomSettings,
...defaultProviderSettings.awsBedrock,
...modelInfoOfDefaultModelNames(defaultModelsOfProvider.awsBedrock),
_didFillInProviderSettings: undefined,
},
}
@@ -350,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
@@ -365,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`)
}
@@ -428,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 = {
@@ -441,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
@@ -491,3 +512,13 @@ export type OverridesOfModel = {
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'
@@ -164,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)
@@ -202,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) {
@@ -260,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,18 +7,18 @@
/* 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 { GoogleGenerativeAI, Tool as GeminiTool, SchemaType, FunctionDeclaration, FunctionDeclarationSchemaProperty } from '@google/generative-ai';
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 { 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 () => {
@@ -48,6 +48,7 @@ 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>
@@ -114,10 +115,36 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
}
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]
@@ -147,7 +174,12 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, overridesOfModel }: SendFIMParams_Internal) => {
const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_, overridesOfModel)
const {
modelName,
supportsFIM,
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
if (!supportsFIM) {
if (modelName === modelName_)
onError({ message: `Model ${modelName} does not support FIM.`, fullError: null })
@@ -156,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,
@@ -198,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[] = []
@@ -209,33 +241,41 @@ const openAITools = (chatMode: ChatMode) => {
return openAITools
}
const rawToolCallObjOf = (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, overridesOfModel }: 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,
additionalOpenAIPayload,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
@@ -243,23 +283,30 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
// reasoning
const { canIOReasoning, openSourceThinkTags } = reasoningCapabilities || {}
const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions, overridesOfModel) // user's modelName_ here
const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
console.log('include', includeInPayload)
console.log('reasoningInfo', reasoningInfo)
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,
}
@@ -274,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
}
@@ -319,7 +366,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
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 },
})
}
@@ -328,7 +375,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
onError({ message: 'Void: Response from model was empty.', fullError: null })
}
else {
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
const toolCall = rawToolCallObjOfParamsStr(toolName, toolParamsStr, toolId)
const toolCallObj = toolCall ? { toolCall } : {}
onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj });
}
@@ -394,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[] = []
@@ -405,20 +452,10 @@ 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, overridesOfModel, 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,
@@ -435,7 +472,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
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
: {}
@@ -459,7 +496,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
// 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
}
@@ -476,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
@@ -526,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
@@ -642,27 +682,26 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider,
// ---------------- GEMINI NATIVE IMPLEMENTATION ----------------
const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => {
const { name, description, params } = toolInfo
const paramsWithType: { [k: string]: FunctionDeclarationSchemaProperty } = {}
for (const key in params) {
paramsWithType[key] = { type: SchemaType.STRING, ...params[key] }
}
return {
name,
description,
parameters: {
type: SchemaType.OBJECT,
properties: paramsWithType,
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): GeminiTool[] | null => {
const allowedTools = availableTools(chatMode)
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 ?? {}) {
@@ -688,6 +727,7 @@ const sendGeminiChat = async ({
providerName,
modelSelectionOptions,
chatMode,
mcpTools,
}: SendChatParams_Internal) => {
if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`)
@@ -700,31 +740,31 @@ const sendGeminiChat = async ({
// reasoningCapabilities,
} = getModelCapabilities(providerName, modelName_, overridesOfModel)
const { providerReasoningIOSettings } = getProviderCapabilities(providerName)
// 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 includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {}
const thinkingConfig: ThinkingConfig | undefined = !reasoningInfo?.isReasoningEnabled ? undefined
: reasoningInfo.type === 'budget_slider_value' ?
{ thinkingBudget: reasoningInfo.reasoningBudget }
: undefined
// tools
const potentialTools = chatMode !== null ? geminiTools(chatMode) : null
const nativeToolsObj = potentialTools && specialToolFormat === 'gemini-style' ?
{ tools: potentialTools } as const
: {}
const potentialTools = geminiTools(chatMode, mcpTools)
const toolConfig = potentialTools && specialToolFormat === 'gemini-style' ?
potentialTools
: undefined
// instance
const genAI = new GoogleGenerativeAI(
thisConfig.apiKey
);
const model = genAI.getGenerativeModel({
systemInstruction: separateSystemMessage,
model: modelName,
});
const genAI = new GoogleGenAI({ apiKey: thisConfig.apiKey });
// 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
}
@@ -735,28 +775,34 @@ const sendGeminiChat = async ({
let toolName = ''
let toolParamsStr = ''
let toolId = ''
model.generateContentStream({
systemInstruction: separateSystemMessage ?? undefined,
contents: messages as any,
...includeInPayload,
...nativeToolsObj,
genAI.models.generateContentStream({
model: modelName,
config: {
systemInstruction: separateSystemMessage,
thinkingConfig: thinkingConfig,
tools: toolConfig,
},
contents: messages as GeminiLLMChatMessage[],
})
.then(async ({ stream, response }) => {
.then(async (stream) => {
_setAborter(() => { stream.return(fullTextSoFar); });
// Process the stream
for await (const chunk of stream) {
// message
const newText = chunk.text() ?? ''
const newText = chunk.text ?? ''
fullTextSoFar += newText
// tool call
const functionCalls = chunk.functionCalls()
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)
@@ -765,7 +811,7 @@ const sendGeminiChat = async ({
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined,
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
})
}
@@ -773,8 +819,8 @@ const sendGeminiChat = async ({
if (!fullTextSoFar && !fullReasoningSoFar && !toolName) {
onError({ message: 'Void: Response from model was empty.', fullError: null })
} else {
const toolId = generateUuid() // gemini does not generate tool IDs. Generate one
const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId)
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 });
}
@@ -886,6 +932,12 @@ export const sendLLMMessageToProviderImplementation = {
sendFIM: null,
list: null,
},
awsBedrock: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
} satisfies CallFnOfProvider
@@ -23,6 +23,7 @@ export const sendLLMMessage = async ({
overridesOfModel,
chatMode,
separateSystemMessage,
mcpTools,
}: SendLLMMessageParams,
metricsService: IMetricsService
@@ -107,7 +108,7 @@ export const sendLLMMessage = async ({
}
const { sendFIM, sendChat } = implementation
if (messagesType === 'chatMessages') {
await sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, overridesOfModel, 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') {
@@ -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)
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