Compare commits

...

97 Commits

Author SHA1 Message Date
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 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 c3125804f3 lint fix 2025-06-23 00:36:58 -04:00
Andrew Pareles ec3b7caa8e fix 2025-06-23 00:26:09 -04:00
Andrew Pareles ec5b7636be disabled fix + bump 2025-06-23 00:18:44 -04:00
Andrew Pareles a475c80f4f bump 1.4.5 2025-06-21 14:56:44 -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 ad8fedc307 add icons 2025-06-19 13:58:41 -07:00
Andrew Pareles 1191abf745 1.4.4 2025-06-09 17:31:01 -07:00
Andrew Pareles 40a27cd1ee mcp tool interrupt fix 2025-06-09 17:30:03 -07:00
Andrew Pareles 97cb63c7fc fix? 2025-06-06 01:42:44 -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
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 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 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
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
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
vrtnis 23bb28f3c0 Merge branch 'main' into void-settings-sidebar 2025-05-23 11:27:39 -07:00
vrtnis 5995591296 feat(void): add Bedrock provider in onboarding + type updates 2025-05-20 01:36:16 +00:00
vrtnis 1d15d29a19 Updated Settings.tsx with new sidebar tabs 2025-05-18 20:46:45 +00: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
Steven Wexler 4300df420c Refine VoidSCM TODO comments 2025-05-12 11:48:47 -06:00
Steven Wexler 74d5c86593 Add generate commit message button for git 2025-05-12 08:44:05 -06:00
34 changed files with 1170 additions and 395 deletions
+4
View File
@@ -10,3 +10,7 @@ In typescript, do NOT cast to types if not neccessary. NEVER lazily cast to 'any
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.
+2 -2
View File
@@ -1,8 +1,8 @@
{
"nameShort": "Void",
"nameLong": "Void",
"voidVersion": "1.3.10",
"voidRelease": "0034",
"voidVersion": "1.4.9",
"voidRelease": "0044",
"applicationName": "void",
"dataFolderName": ".void-editor",
"win32MutexName": "voideditor",
+8 -1
View File
@@ -130,8 +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).
@@ -1103,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));
@@ -1244,6 +1246,11 @@ 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);
@@ -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}
 
${restartString}
${telemetryTableDescription}
${telemetryTable}
 
${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);
}
@@ -641,7 +641,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
// 2. if tool requires approval, break from the loop, awaiting approval
const approvalType = isBuiltInTool ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
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)
@@ -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; }) => {
@@ -34,6 +34,7 @@ import ErrorBoundary from './ErrorBoundary.js';
import { ToolApprovalTypeSwitch } from '../void-settings-tsx/Settings.js';
import { persistentTerminalNameOfId } from '../../../terminalToolService.js';
import { removeMCPToolNamePrefix } from '../../../../common/mcpServiceTypes.js';
@@ -1616,7 +1617,7 @@ const ToolRequestAcceptRejectButtons = ({ toolName }: { toolName: ToolName }) =>
</button>
)
const approvalType = isABuiltinToolName(toolName) ? approvalTypeOfBuiltinToolName[toolName] : 'mcp-tools'
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 ${approvalType}`} />
</div> : null
@@ -1857,7 +1858,7 @@ const MCPToolWrapper = ({ toolMessage }: WrapperProps<string>) => {
const mcpService = accessor.get('IMCPService')
const title = getTitle(toolMessage)
const desc1 = toolMessage.name
const desc1 = removeMCPToolNamePrefix(toolMessage.name)
const icon = null
@@ -5,7 +5,7 @@
import React, { useState, useEffect, useCallback } from 'react'
import { MCPUserState, RefreshableProviderName, SettingsOfProvider } from '../../../../../../../workbench/contrib/void/common/voidSettingsTypes.js'
import { IDisposable } from '../../../../../../../base/common/lifecycle.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'
@@ -52,6 +52,8 @@ 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
@@ -226,6 +228,8 @@ const getReactAccessor = (accessor: ServicesAccessor) => {
IExtensionTransferService: accessor.get(IExtensionTransferService),
IMCPService: accessor.get(IMCPService),
IStorageService: accessor.get(IStorageService),
} as const
return reactAccessor
}
@@ -399,3 +403,26 @@ export const useMCPServiceState = () => {
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 }) => {
@@ -7,7 +7,7 @@ import React, { useCallback, useEffect, useMemo, useState, useRef } from 'react'
import { ProviderName, SettingName, displayInfoOfSettingName, providerNames, VoidStatefulModelInfo, customSettingNamesOfProvider, RefreshableProviderName, refreshableProviderNames, displayInfoOfProviderName, nonlocalProviderNames, localProviderNames, GlobalSettingName, featureNames, displayInfoOfFeatureName, isProviderNameDisabled, FeatureName, hasDownloadButtonsOnModelsProviderNames, subTextMdOfProviderName } from '../../../../common/voidSettingsTypes.js'
import ErrorBoundary from '../sidebar-tsx/ErrorBoundary.js'
import { VoidButtonBgDarken, VoidCustomDropdownBox, VoidInputBox2, VoidSimpleInputBox, VoidSwitch } from '../util/inputs.js'
import { useAccessor, useIsDark, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { useAccessor, useIsDark, useIsOptedOut, useRefreshModelListener, useRefreshModelState, useSettingsState } from '../util/services.js'
import { X, RefreshCw, Loader2, Check, Asterisk, Plus } from 'lucide-react'
import { URI } from '../../../../../../../base/common/uri.js'
import { ModelDropdown } from './ModelDropdown.js'
@@ -21,6 +21,18 @@ import { getModelCapabilities, modelOverrideKeys, ModelOverrides } from '../../.
import { TransferEditorType, TransferFilesInfo } from '../../../extensionTransferTypes.js';
import { MCPServer } from '../../../../common/mcpServiceTypes.js';
import { useMCPServiceState } from '../util/services.js';
import { OPT_OUT_KEY } from '../../../../common/storageKeys.js';
import { StorageScope, StorageTarget } from '../../../../../../../platform/storage/common/storage.js';
type Tab =
| 'models'
| 'localProviders'
| 'providers'
| 'featureOptions'
| 'mcp'
| 'general'
| 'all';
const ButtonLeftTextRightOption = ({ text, leftButton }: { text: string, leftButton?: React.ReactNode }) => {
@@ -205,10 +217,10 @@ const SimpleModelSettingsDialog = ({
if (!isOpen || !modelInfo) return null;
const { modelName, providerName, type } = modelInfo;
const accessor = useAccessor();
const settingsState = useSettingsState();
const accessor = useAccessor()
const settingsState = useSettingsState()
const mouseDownInsideModal = useRef(false); // Ref to track mousedown origin
const settingsStateService = accessor.get('IVoidSettingsService');
const settingsStateService = accessor.get('IVoidSettingsService')
// current overrides and defaults
const defaultModelCapabilities = getModelCapabilities(providerName, modelName, undefined);
@@ -678,6 +690,7 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam
// </div >
// }
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
const voidSettingsState = useSettingsState()
@@ -1018,6 +1031,20 @@ const MCPServersList = () => {
export const Settings = () => {
const isDark = useIsDark()
// ─── sidebar nav ──────────────────────────
const [selectedSection, setSelectedSection] =
useState<Tab>('models');
const navItems: { tab: Tab; label: string }[] = [
{ tab: 'models', label: 'Models' },
{ tab: 'localProviders', label: 'Local Providers' },
{ tab: 'providers', label: 'Main Providers' },
{ tab: 'featureOptions', label: 'Feature Options' },
{ tab: 'general', label: 'General' },
{ tab: 'mcp', label: 'MCP' },
{ tab: 'all', label: 'All Settings' },
];
const shouldShowTab = (tab: Tab) => selectedSection === 'all' || selectedSection === tab;
const accessor = useAccessor()
const commandService = accessor.get('ICommandService')
const environmentService = accessor.get('IEnvironmentService')
@@ -1027,6 +1054,9 @@ export const Settings = () => {
const chatThreadsService = accessor.get('IChatThreadService')
const notificationService = accessor.get('INotificationService')
const mcpService = accessor.get('IMCPService')
const storageService = accessor.get('IStorageService')
const metricsService = accessor.get('IMetricsService')
const isOptedOut = useIsOptedOut()
const onDownload = (t: 'Chats' | 'Settings') => {
let dataStr: string
@@ -1092,317 +1122,442 @@ export const Settings = () => {
}
return <div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%' }}>
<div className='overflow-y-auto w-full h-full px-10 py-10 select-none'>
return (
<div className={`@@void-scope ${isDark ? 'dark' : ''}`} style={{ height: '100%', width: '100%', overflow: 'auto' }}>
<div className="flex flex-col md:flex-row w-full gap-6 max-w-[900px] mx-auto mb-32" style={{ minHeight: '80vh' }}>
{/* ────────────── SIDEBAR ────────────── */}
<div className='max-w-xl mx-auto'>
<aside className="md:w-1/4 w-full p-6 shrink-0">
{/* vertical tab list */}
<div className="flex flex-col gap-2 mt-12">
{navItems.map(({ tab, label }) => (
<button
key={tab}
onClick={() => {
if (tab === 'all') {
setSelectedSection('all');
window.scrollTo({ top: 0, behavior: 'smooth' });
} else {
setSelectedSection(tab);
}
}}
className={`
py-2 px-4 rounded-md text-left transition-all duration-200
${selectedSection === tab
? 'bg-[#0e70c0]/80 text-white font-medium shadow-sm'
: 'bg-void-bg-2 hover:bg-void-bg-2/80 text-void-fg-1'}
`}
>
{label}
</button>
))}
</div>
</aside>
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
<div className='w-full h-[1px] my-2' />
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<RedoOnboardingButton />
</ErrorBoundary>
<div className='w-full h-[1px] my-4' />
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ModelDump />
<div className='w-full h-[1px] my-4' />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Local Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='opacity-80 mb-4'>
<OllamaSetupInstructions sayWeAutoDetect={true} />
</div>
<ErrorBoundary>
<VoidProviderSettings providerNames={localProviderNames} />
</ErrorBoundary>
<h2 className={`text-3xl mb-2 mt-12`}>Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
<ErrorBoundary>
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
{/* ───────────── MAIN PANE ───────────── */}
<main className="flex-1 p-6 select-none">
<h2 className={`text-3xl mt-12`}>Feature Options</h2>
<div className='max-w-3xl'>
<div className='flex flex-col gap-y-8 my-4'>
<ErrorBoundary>
{/* FIM */}
<div>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>
<span>
Experimental.{' '}
</span>
<span
className='hover:brightness-110'
data-tooltip-id='void-tooltip'
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
data-tooltip-class-name='void-max-w-[300px]'
>
Only works with FIM models.*
</span>
<h1 className='text-2xl w-full'>{`Void's Settings`}</h1>
<div className='w-full h-[1px] my-2' />
{/* Models section (formerly FeaturesTab) */}
<ErrorBoundary>
<RedoOnboardingButton />
</ErrorBoundary>
<div className='w-full h-[1px] my-4' />
{/* All sections in flex container with gap-12 */}
<div className='flex flex-col gap-12'>
{/* Models section (formerly FeaturesTab) */}
<div className={shouldShowTab('models') ? `` : 'hidden'}>
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Models</h2>
<ModelDump />
<div className='w-full h-[1px] my-4' />
<AutoDetectLocalModelsToggle />
<RefreshableModels />
</ErrorBoundary>
</div>
<div className='my-2'>
{/* Enable Switch */}
{/* Local Providers section */}
<div className={shouldShowTab('localProviders') ? `` : 'hidden'}>
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.enableAutocomplete}
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
<h2 className={`text-3xl mb-2`}>Local Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access any model that you host locally. We automatically detect your local models by default.`}</h3>
<div className='opacity-80 mb-4'>
<OllamaSetupInstructions sayWeAutoDetect={true} />
</div>
<VoidProviderSettings providerNames={localProviderNames} />
</ErrorBoundary>
</div>
{/* Main Providers section */}
<div className={shouldShowTab('providers') ? `` : 'hidden'}>
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Main Providers</h2>
<h3 className={`text-void-fg-3 mb-2`}>{`Void can access models from Anthropic, OpenAI, OpenRouter, and more.`}</h3>
<VoidProviderSettings providerNames={nonlocalProviderNames} />
</ErrorBoundary>
</div>
{/* Feature Options section */}
<div className={shouldShowTab('featureOptions') ? `` : 'hidden'}>
<ErrorBoundary>
<h2 className={`text-3xl mb-2`}>Feature Options</h2>
<div className='flex flex-col gap-y-8 my-4'>
<ErrorBoundary>
{/* FIM */}
<div>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Autocomplete')}</h4>
<div className='text-sm text-void-fg-3 mt-1'>
<span>
Experimental.{' '}
</span>
<span
className='hover:brightness-110'
data-tooltip-id='void-tooltip'
data-tooltip-content='We recommend using the largest qwen2.5-coder model you can with Ollama (try qwen2.5-coder:3b).'
data-tooltip-class-name='void-max-w-[20px]'
>
Only works with FIM models.*
</span>
</div>
<div className='my-2'>
{/* Enable Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.enableAutocomplete}
onChange={(newVal) => voidSettingsService.setGlobalSetting('enableAutocomplete', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.enableAutocomplete ? 'Enabled' : 'Disabled'}</span>
</div>
</ErrorBoundary>
{/* Model Dropdown */}
<ErrorBoundary>
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</ErrorBoundary>
</div>
</div>
</ErrorBoundary>
{/* Apply */}
<ErrorBoundary>
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.syncApplyToChat}
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
<div className='my-2'>
{/* Fast Apply Method Dropdown */}
<div className='flex items-center gap-x-2 my-2'>
<FastApplyMethodDropdown />
</div>
</div>
</div>
</ErrorBoundary>
{/* Tools Section */}
<div>
<h4 className={`text-base`}>Tools</h4>
<div className='text-sm text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<ErrorBoundary>
{[...toolApprovalTypes].map((approvalType) => {
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
</div>
})}
</ErrorBoundary>
{/* Tool Lint Errors Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.includeToolLintErrors}
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
</div>
</ErrorBoundary>
{/* Auto Accept LLM Changes Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.autoAcceptLLMChanges}
onChange={(newVal) => voidSettingsService.setGlobalSetting('autoAcceptLLMChanges', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>Auto-accept LLM changes</span>
</div>
</ErrorBoundary>
</div>
</div>
<div className='w-full'>
<h4 className={`text-base`}>Editor</h4>
<div className='text-sm text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.showInlineSuggestions}
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
</div>
</ErrorBoundary>
</div>
</div>
{/* SCM */}
<ErrorBoundary>
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('SCM')}</h4>
<div className='text-sm text-void-fg-3 mt-1'>Settings that control the behavior of the commit message generator.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.syncSCMToChat}
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncSCMToChat', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncSCMToChat ? 'Same as Chat model' : 'Different model'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${settingsState.globalSettings.syncSCMToChat ? 'hidden' : ''}`}>
<ModelDropdown featureName={'SCM'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
</div>
</div>
</div>
</ErrorBoundary>
</div>
</ErrorBoundary>
</div>
{/* Model Dropdown */}
<ErrorBoundary>
<div className={`my-2 ${!settingsState.globalSettings.enableAutocomplete ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Autocomplete'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
{/* General section */}
<div className={`${shouldShowTab('general') ? `` : 'hidden'} flex flex-col gap-12`}>
{/* One-Click Switch section */}
<div>
<ErrorBoundary>
<h2 className='text-3xl mb-2'>One-Click Switch</h2>
<h4 className='text-void-fg-3 mb-4'>{`Transfer your editor settings into Void.`}</h4>
<div className='flex flex-col gap-2'>
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
</div>
</ErrorBoundary>
</div>
{/* Import/Export section */}
<div>
<h2 className='text-3xl mb-2'>Import/Export</h2>
<h4 className='text-void-fg-3 mb-4'>{`Transfer Void's settings and chats in and out of Void.`}</h4>
<div className='flex flex-col gap-8'>
{/* Settings Subcategory */}
<div className='flex flex-col gap-2 max-w-48 w-full'>
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
Import Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
Export Settings
</VoidButtonBgDarken>
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
Reset Settings
</ConfirmButton>
</div>
{/* Chats Subcategory */}
<div className='flex flex-col gap-2 max-w-48 w-full'>
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
Import Chats
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
Export Chats
</VoidButtonBgDarken>
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
Reset Chats
</ConfirmButton>
</div>
</div>
</ErrorBoundary>
</div>
</div>
</ErrorBoundary>
{/* Apply */}
<ErrorBoundary>
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the Apply button.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.syncApplyToChat}
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncApplyToChat', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncApplyToChat ? 'Same as Chat model' : 'Different model'}</span>
</div>
{/* Model Dropdown */}
<div className={`my-2 ${settingsState.globalSettings.syncApplyToChat ? 'hidden' : ''}`}>
<ModelDropdown featureName={'Apply'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
{/* Built-in Settings section */}
<div>
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<ErrorBoundary>
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
General Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
Keyboard Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
Theme Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
Open Logs
</VoidButtonBgDarken>
</div>
</ErrorBoundary>
</div>
</div>
<div className='my-2'>
{/* Fast Apply Method Dropdown */}
<div className='flex items-center gap-x-2 my-2'>
<FastApplyMethodDropdown />
</div>
</div>
{/* Metrics section */}
<div className='max-w-[600px]'>
<h2 className={`text-3xl mb-2`}>Metrics</h2>
<h4 className={`text-void-fg-3 mb-4`}>Very basic anonymous usage tracking helps us keep Void running smoothly. You may opt out below. Regardless of this setting, Void never sees your code, messages, or API keys.</h4>
</div>
</ErrorBoundary>
{/* Tools Section */}
<div>
<h4 className={`text-base`}>Tools</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>{`Tools are functions that LLMs can call. Some tools require user approval.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<ErrorBoundary>
{[...toolApprovalTypes].map((approvalType) => {
return <div key={approvalType} className="flex items-center gap-x-2 my-2">
<ToolApprovalTypeSwitch size='xs' approvalType={approvalType} desc={`Auto-approve ${approvalType}`} />
<div className='my-2'>
{/* Disable All Metrics Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={isOptedOut}
onChange={(newVal) => {
storageService.store(OPT_OUT_KEY, newVal, StorageScope.APPLICATION, StorageTarget.MACHINE)
metricsService.capture(`Set metrics opt-out to ${newVal}`, {}) // this only fires if it's enabled, so it's fine to have here
}}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{'Opt-out (requires restart)'}</span>
</div>
</ErrorBoundary>
</div>
})}
</ErrorBoundary>
{/* Tool Lint Errors Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.includeToolLintErrors}
onChange={(newVal) => voidSettingsService.setGlobalSetting('includeToolLintErrors', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.includeToolLintErrors ? 'Fix lint errors' : `Fix lint errors`}</span>
</div>
</ErrorBoundary>
</div>
</div>
<div className='w-full'>
<h4 className={`text-base`}>Editor</h4>
<div className='text-sm italic text-void-fg-3 mt-1'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
<ErrorBoundary>
<div className='flex items-center gap-x-2 my-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.showInlineSuggestions}
onChange={(newVal) => voidSettingsService.setGlobalSetting('showInlineSuggestions', newVal)}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.showInlineSuggestions ? 'Show suggestions on select' : 'Show suggestions on select'}</span>
</div>
</ErrorBoundary>
</div>
</div>
</div>
{/* General section (formerly GeneralTab) */}
<div className='mt-12'>
<ErrorBoundary>
<h2 className='text-3xl mb-2 mt-12'>One-Click Switch</h2>
<h4 className='text-void-fg-3 mb-4'>{`Transfer your editor settings into Void.`}</h4>
<div className='flex flex-col gap-2'>
<OneClickSwitchButton className='w-48' fromEditor="VS Code" />
<OneClickSwitchButton className='w-48' fromEditor="Cursor" />
<OneClickSwitchButton className='w-48' fromEditor="Windsurf" />
</div>
</ErrorBoundary>
</div>
{/* Import/Export section, as its own block right after One-Click Switch */}
<div className='mt-12'>
<h2 className='text-3xl mb-2'>Import/Export</h2>
<h4 className='text-void-fg-3 mb-4'>{`Transfer Void's settings and chats in and out of Void.`}</h4>
<div className='flex flex-col gap-8'>
{/* Settings Subcategory */}
<div className='flex flex-col gap-2 max-w-48 w-full'>
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
Import Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Settings')}>
Export Settings
</VoidButtonBgDarken>
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { voidSettingsService.resetState(); }}>
Reset Settings
</ConfirmButton>
</div>
{/* Chats Subcategory */}
<div className='flex flex-col gap-2 w-full max-w-48'>
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
Import Chats
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => onDownload('Chats')}>
Export Chats
</VoidButtonBgDarken>
<ConfirmButton className='px-4 py-1 w-full' onConfirm={() => { chatThreadsService.resetState(); }}>
Reset Chats
</ConfirmButton>
</div>
</div>
</div>
<div className='mt-12'>
<h2 className={`text-3xl mb-2`}>Built-in Settings</h2>
<h4 className={`text-void-fg-3 mb-4`}>{`IDE settings, keyboard settings, and theme customization.`}</h4>
<ErrorBoundary>
<div className='flex flex-col gap-2 justify-center max-w-48 w-full'>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openSettings') }}>
General Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.openGlobalKeybindings') }}>
Keyboard Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { commandService.executeCommand('workbench.action.selectTheme') }}>
Theme Settings
</VoidButtonBgDarken>
<VoidButtonBgDarken className='px-4 py-1' onClick={() => { nativeHostService.showItemInFolder(environmentService.logsHome.fsPath) }}>
Open Logs
</VoidButtonBgDarken>
</div>
</ErrorBoundary>
</div>
<div className='mt-12 max-w-[600px]'>
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
<h4 className={`text-void-fg-3 mb-4`}>
<ChatMarkdownRender inPTag={true} string={`
{/* AI Instructions section */}
<div className='max-w-[600px]'>
<h2 className={`text-3xl mb-2`}>AI Instructions</h2>
<h4 className={`text-void-fg-3 mb-4`}>
<ChatMarkdownRender inPTag={true} string={`
System instructions to include with all AI requests.
Alternatively, place a \`.voidrules\` file in the root of your workspace.
`} chatMessageLocation={undefined} />
</h4>
<ErrorBoundary>
<AIInstructionsBox />
</ErrorBoundary>
{/* --- Disable System Message Toggle --- */}
<div className='my-4'>
<ErrorBoundary>
<div className='flex items-center gap-x-2'>
<VoidSwitch
size='xs'
value={settingsState.globalSettings.disableSystemMessage}
onChange={(newValue) => {
voidSettingsService.setGlobalSetting('disableSystemMessage', newValue);
}}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>
{settingsState.globalSettings.disableSystemMessage ? 'Disable system message' : 'Disable system message'}
</span>
</div>
</ErrorBoundary>
<div className='text-void-fg-3 text-xs mt-1'>
{`When disabled, Void will not include anything in the system message except for content you specified above.`}
</div>
</div>
</div>
</h4>
<ErrorBoundary>
<AIInstructionsBox />
</ErrorBoundary>
{/* --- Disable System Message Toggle --- */}
<div className='my-4'>
<ErrorBoundary>
<div className='flex items-center gap-x-2'>
<VoidSwitch
size='xs'
value={!!settingsState.globalSettings.disableSystemMessage}
onChange={(newValue) => {
voidSettingsService.setGlobalSetting('disableSystemMessage', newValue);
}}
/>
<span className='text-void-fg-3 text-xs pointer-events-none'>
{'Disable system message'}
</span>
</div>
</ErrorBoundary>
<div className='text-void-fg-3 text-xs mt-1'>
{`When disabled, Void will not include anything in the system message except for content you specified above.`}
</div>
</div>
</div>
<div className='mt-12'>
<h2 className='text-3xl mb-2'>MCP</h2>
<h4 className={`text-void-fg-3 mb-4`}>
<ChatMarkdownRender inPTag={true} string={`
</div>
{/* MCP section */}
<div className={shouldShowTab('mcp') ? `` : 'hidden'}>
<ErrorBoundary>
<h2 className='text-3xl mb-2'>MCP</h2>
<h4 className={`text-void-fg-3 mb-4`}>
<ChatMarkdownRender inPTag={true} string={`
Use Model Context Protocol to provide Agent mode with more tools.
`} chatMessageLocation={undefined} />
</h4>
<div className='my-2'>
<VoidButtonBgDarken className='px-4 py-1 w-full max-w-48' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
Add MCP Server
</VoidButtonBgDarken>
</div>
</h4>
<div className='my-2'>
<VoidButtonBgDarken className='px-4 py-1 w-full max-w-48' onClick={async () => { await mcpService.revealMCPConfigFile() }}>
Add MCP Server
</VoidButtonBgDarken>
</div>
<ErrorBoundary>
<MCPServersList />
</ErrorBoundary>
</div>
<ErrorBoundary>
<MCPServersList />
</ErrorBoundary>
</ErrorBoundary>
</div>
</div>
</div>
</main>
</div>
</div>
</div>
);
}
@@ -297,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 };
@@ -42,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) => {
@@ -133,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 = {
@@ -61,6 +61,9 @@ 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)
@@ -88,7 +88,7 @@ class MCPService extends Disposable implements IMCPService {
const onEvent = (e: MCPServerEventResponse) => {
console.log('GOT EVENT', e)
// console.log('GOT EVENT', e)
this._setMCPServerState(e.response.name, e.response.newServer)
}
this._register((this.channel.listen('onAdd_server') satisfies Event<MCPServerEventResponse>)(onEvent));
@@ -234,3 +234,9 @@ export interface MCPToolCallParams {
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
@@ -145,6 +151,7 @@ export const defaultModelsOfProvider = {
openAICompatible: [], // fallback
googleVertex: [],
microsoftAzure: [],
awsBedrock: [],
liteLLM: [],
@@ -1098,6 +1105,18 @@ const microsoftAzureSettings: VoidStaticProviderInfo = {
},
}
// ---------------- AWS BEDROCK ----------------
const awsBedrockModelOptions = {
} as const satisfies Record<string, VoidStaticModelInfo>
const awsBedrockSettings: VoidStaticProviderInfo = {
modelOptions: awsBedrockModelOptions,
modelOptionsFallback: (modelName) => { return null },
providerReasoningIOSettings: {
input: { includeInPayload: openAICompatIncludeInPayloadReasoning },
},
}
// ---------------- VLLM, OLLAMA, OPENAICOMPAT (self-hosted / local) ----------------
const ollamaModelOptions = {
@@ -1214,6 +1233,7 @@ const openaiCompatible: VoidStaticProviderInfo = {
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' },
},
}
@@ -1437,6 +1457,7 @@ const modelSettingsOfProvider: { [providerName in ProviderName]: VoidStaticProvi
googleVertex: googleVertexSettings,
microsoftAzure: microsoftAzureSettings,
awsBedrock: awsBedrockSettings,
} as const
@@ -980,3 +980,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()
}
@@ -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'
@@ -18,7 +18,7 @@ export type ShallowDirectoryItem = {
}
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'mcp-tools' }> = {
export const approvalTypeOfBuiltinToolName: Partial<{ [T in BuiltinToolName]?: 'edits' | 'terminal' | 'MCP tools' }> = {
'create_file_or_folder': 'edits',
'delete_file_or_folder': 'edits',
'rewrite_file': 'edits',
@@ -35,7 +35,7 @@ export type ToolApprovalType = NonNullable<(typeof approvalTypeOfBuiltinToolName
export const toolApprovalTypes = new Set<ToolApprovalType>([
...Object.values(approvalTypeOfBuiltinToolName),
'mcp-tools',
'MCP tools',
])
@@ -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')
@@ -116,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, },
}
@@ -213,9 +214,9 @@ 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: {},
@@ -262,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())
@@ -280,7 +282,16 @@ 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()
@@ -392,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) => {
@@ -409,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()
}
@@ -428,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()
}
}
@@ -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,6 +443,7 @@ export type GlobalSettings = {
aiInstructions: string;
enableAutocomplete: boolean;
syncApplyToChat: boolean;
syncSCMToChat: boolean;
enableFastApply: boolean;
chatMode: ChatMode;
autoApprove: { [approvalType in ToolApprovalType]?: boolean };
@@ -435,6 +451,7 @@ export type GlobalSettings = {
includeToolLintErrors: boolean;
isOnboardingComplete: boolean;
disableSystemMessage: boolean;
autoAcceptLLMChanges: boolean;
}
export const defaultGlobalSettings: GlobalSettings = {
@@ -442,6 +459,7 @@ export const defaultGlobalSettings: GlobalSettings = {
aiInstructions: '',
enableAutocomplete: false,
syncApplyToChat: true,
syncSCMToChat: true,
enableFastApply: true,
chatMode: 'agent',
autoApprove: {},
@@ -449,6 +467,7 @@ export const defaultGlobalSettings: GlobalSettings = {
includeToolLintErrors: true,
isOnboardingComplete: false,
disableSystemMessage: false,
autoAcceptLLMChanges: false,
}
export type GlobalSettingName = keyof GlobalSettings
@@ -122,6 +122,29 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ
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]
@@ -343,7 +366,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
})
}
@@ -490,7 +513,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag
onText({
fullText,
fullReasoning,
toolCall: { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
toolCall: !fullToolName ? undefined : { name: fullToolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' },
})
}
// there are no events for tool_use, it comes in at the end
@@ -540,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')
// 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
@@ -785,7 +811,7 @@ const sendGeminiChat = async ({
onText({
fullText: fullTextSoFar,
fullReasoning: fullReasoningSoFar,
toolCall: { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
toolCall: !toolName ? undefined : { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: toolId },
})
}
@@ -906,6 +932,12 @@ export const sendLLMMessageToProviderImplementation = {
sendFIM: null,
list: null,
},
awsBedrock: {
sendChat: (params) => _sendOpenAICompatibleChat(params),
sendFIM: null,
list: null,
},
} satisfies CallFnOfProvider
@@ -13,7 +13,7 @@ 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 } from '../common/mcpServiceTypes.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';
@@ -195,7 +195,7 @@ export class MCPChannel implements IServerChannel {
}
}
} else if (server.command) {
console.log('ENV DATA: ', server.env)
// console.log('ENV DATA: ', server.env)
transport = new StdioClientTransport({
command: server.command,
args: server.args,
@@ -310,7 +310,7 @@ export class MCPChannel implements IServerChannel {
// Call the tool with the provided parameters
const response = await client.callTool({
name: toolName,
name: removeMCPToolNamePrefix(toolName),
arguments: params
})
const { content } = response as CallToolResult
@@ -320,8 +320,7 @@ export class MCPChannel implements IServerChannel {
// handle text response
if (response.isError) {
throw new Error(`Tool call error: ${response.content}`)
// handle error
throw new Error(`Tool call error: ${returnValue.text}`)
}
// handle success
@@ -354,41 +353,26 @@ export class MCPChannel implements IServerChannel {
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
if (err && typeof err === 'object' && 'code' in err) {
const errorCode = err.code;
const errorName = err.name || 'Unknown Error';
const errorMsg = err.message || '';
// Map common JSON-RPC error codes to user-friendly messages
let codeDescription = '';
switch (errorCode) {
case -32700:
codeDescription = 'Parse Error';
break;
case -32600:
codeDescription = 'Invalid Request';
break;
case -32601:
codeDescription = 'Method Not Found';
break;
case -32602:
codeDescription = 'Invalid Parameters';
break;
case -32603:
codeDescription = 'Internal Error';
break;
default:
codeDescription = `Error Code ${errorCode}`;
}
errorMessage = `${errorName} (${codeDescription})${errorMsg ? ': ' + errorMsg : ''}`;
} else if (err && typeof err === 'object' && 'message' in err) {
// Standard error with message
errorMessage = err.message;
} else if (typeof err === 'string') {
else if (typeof err === 'string') {
// String error
errorMessage = err;
} else {
@@ -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