From 74d5c865932e9c72869be9359f04c81b96b3b7a5 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Mon, 12 May 2025 08:44:05 -0600 Subject: [PATCH 01/23] Add generate commit message button for git --- src/vs/code/electron-main/app.ts | 7 +- .../src/void-onboarding/VoidOnboarding.tsx | 1 + .../contrib/void/browser/void.contribution.ts | 3 + .../workbench/contrib/void/browser/voidSCM.ts | 131 ++++++++++++++++++ .../contrib/void/common/prompt/prompts.ts | 17 +++ .../workbench/contrib/void/common/voidSCM.ts | 19 +++ .../void/common/voidSettingsService.ts | 11 +- .../contrib/void/common/voidSettingsTypes.ts | 5 +- .../contrib/void/electron-main/voidSCM.ts | 56 ++++++++ 9 files changed, 246 insertions(+), 4 deletions(-) create mode 100644 src/vs/workbench/contrib/void/browser/voidSCM.ts create mode 100644 src/vs/workbench/contrib/void/common/voidSCM.ts create mode 100644 src/vs/workbench/contrib/void/electron-main/voidSCM.ts diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 4feafb34..5406d2a8 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -130,7 +130,8 @@ 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 { IVoidSCM } from '../../workbench/contrib/void/common/voidSCM.js'; +import { VoidSCM } from '../../workbench/contrib/void/electron-main/voidSCM.js'; /** * The main VS Code application. There will only ever be one instance, * even if the user starts many instances (e.g. from the command line). @@ -1102,6 +1103,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(IVoidSCM, new SyncDescriptor(VoidSCM, undefined, false)); // Default Extensions Profile Init services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); @@ -1243,6 +1245,9 @@ export class CodeApplication extends Disposable { const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel); + const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCM), disposables); + mainProcessElectronServer.registerChannel('void-channel-scm', voidSCMChannel); + // Extension Host Debug Broadcasting const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService)); mainProcessElectronServer.registerChannel('extensionhostdebugservice', electronExtensionHostDebugBroadcastChannel); diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx index d16ebefd..a97f94ed 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-onboarding/VoidOnboarding.tsx @@ -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 }) => { diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index c0e84606..d3fc1e2e 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -58,6 +58,9 @@ import './voidOnboardingService.js' // register misc service import './miscWokrbenchContrib.js' +// register source control management +import './voidSCM.js' + // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- // llmMessage diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCM.ts new file mode 100644 index 00000000..67bfe175 --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidSCM.ts @@ -0,0 +1,131 @@ +import { ThemeIcon } from '../../../../base/common/themables.js' +import { localize2 } from '../../../../nls.js' +import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js' +import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js' +import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.js' +import { ISCMService } from '../../scm/common/scm.js' +import { ProxyChannel } from '../../../../base/parts/ipc/common/ipc.js' +import { IVoidSCM } from '../common/voidSCM.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 { commitMessage_systemMessage } from '../common/prompt/prompts.js' +import { LLMChatMessage } from '../common/sendLLMMessageTypes.js' +import { ISCMRepository } from '../../../../workbench/contrib/scm/common/scm.js' + +interface ModelOptions { + modelSelection: ModelSelection | null + modelSelectionOptions?: ModelSelectionOptions + overridesOfModel: OverridesOfModel +} + +const scm = 'SCM' + +const prepareModelOptions = (settingsService: IVoidSettingsService): ModelOptions => { + const modelSelection = settingsService.state.modelSelectionOfFeature[scm] + const modelSelectionOptions = modelSelection ? settingsService.state.optionsOfModelSelection[scm][modelSelection?.providerName]?.[modelSelection.modelName] : undefined + const overridesOfModel = settingsService.state.overridesOfModel + return { + modelSelection, + modelSelectionOptions, + overridesOfModel + } +} + +const preparePrompt = (stat: string, sampledDiffs: string) => { + const section1 = `Section 1 - Summary of Changes (git diff --stat):` + const section2 = `Section 2 - Sampled File Diffs (Top changed files):` + 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} +`.trim() +} + +const prepareMessages = (prompt: string, modelOptions: ModelOptions, convertToLLMMessageService: IConvertToLLMMessageService) => { + const simpleMessages = [{ role: 'user' as 'user', content: prompt }] + const { messages, separateSystemMessage } = convertToLLMMessageService.prepareLLMSimpleMessages({ + simpleMessages, + systemMessage: commitMessage_systemMessage, + modelSelection: modelOptions.modelSelection, + featureName: scm, + }) + return { + messages, + separateSystemMessage + } +} + +const onFinalMessage = (repo: ISCMRepository) => (params: { fullText: string }) => { + const match = params.fullText.match(/([\s\S]*?)<\/output>/i) + const commitMessage = match ? match[1].trim() : '' + repo.input.setValue(commitMessage, false) +} + +const sendLLMMessage = (messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions, repo: ISCMRepository, llmMessageService: ILLMMessageService) => { + //TODO VoidSCM - Experiment with messages to get better results + llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + messages, + separateSystemMessage, + chatMode: null, + modelSelection: modelOptions.modelSelection, + modelSelectionOptions: modelOptions.modelSelectionOptions, + overridesOfModel: modelOptions.overridesOfModel, + onText: () => { }, + onFinalMessage: onFinalMessage(repo), + onError: (error: any) => { console.error(error) }, //TODO VoidSCM - handle errors + onAbort: () => { console.log('abort') }, //TODO VoidSCM - handle abort + logging: { loggingName: 'VoidSCM - Commit Message' }, + }) +} + + +class CommitMessagePromptActionService extends Action2 { + private static readonly VOID_COMMIT_MESSAGE_PROMPT_ID = 'void.commitMessagePrompt' + + constructor() { + super({ + id: CommitMessagePromptActionService.VOID_COMMIT_MESSAGE_PROMPT_ID, + 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.equals('scmProvider', 'git'), + group: 'inline' + }] + }) + } + + //TODO VoidSCM - handle loading state, debouncing, errors, and cancellations + async run(accessor: ServicesAccessor): Promise { + const scmService = accessor.get(ISCMService) + const mainProcessService = accessor.get(IMainProcessService) + const voidSettingsService = accessor.get(IVoidSettingsService) + const convertToLLMMessageService = accessor.get(IConvertToLLMMessageService) + const llmMessageService = accessor.get(ILLMMessageService) + const voidSCM = ProxyChannel.toService(mainProcessService.getChannel('void-channel-scm')) + + const repo = Array.from(scmService.repositories || []).find((r: any) => r.provider.contextValue === 'git') + //TODO VoidSCM - use the notification service to show an error if repo or rootUri is not found + if (!repo || !repo.provider.rootUri?.fsPath) { return } + const path = repo.provider.rootUri.fsPath + const [stat, sampledDiffs] = await Promise.all([voidSCM.gitStat(path), voidSCM.gitSampledDiffs(path)]) + const modelOptions = prepareModelOptions(voidSettingsService) + const prompt = preparePrompt(stat, sampledDiffs) + const { messages, separateSystemMessage } = prepareMessages(prompt, modelOptions, convertToLLMMessageService) + sendLLMMessage(messages, separateSystemMessage!, modelOptions, repo, llmMessageService) + } +} + +registerAction2(CommitMessagePromptActionService) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index b83c1a0f..ee269fcd 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -947,3 +947,20 @@ Store Result: After computing fib(n), the result is stored in memo for future re ## END EXAMPLES */ + + +// ======================================================== scm ======================================================================== + +export const commitMessage_systemMessage = ` +You are an expert software engineer AI assistant responsible for writing clear, concise Git commit messages. + +You always respond with: +- The commit message wrapped in tags +- A brief explanation of the reasoning behind the message, wrapped in tags + +Example format: +Fix login bug and improve error handling +This commit updates the login handler to fix a redirect issue and improves frontend error messages for failed logins. + +Do not include anything else outside of these tags. +Never include quotes, markdown, commentary, or explanations outside of and .`.trim() diff --git a/src/vs/workbench/contrib/void/common/voidSCM.ts b/src/vs/workbench/contrib/void/common/voidSCM.ts new file mode 100644 index 00000000..da11dedf --- /dev/null +++ b/src/vs/workbench/contrib/void/common/voidSCM.ts @@ -0,0 +1,19 @@ +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +export interface IVoidSCM { + readonly _serviceBrand: undefined; + /** + * Get git diff --stat + * + * @param path Path to the git repository + */ + gitStat(path: string): Promise + /** + * 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 +} + +export const IVoidSCM = createDecorator('void-scm') diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 73c9eceb..093e40d9 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -110,6 +110,7 @@ export const modelFilterOfFeatureName: { 'Chat': { filter: o => true, emptyMessage: null, }, 'Ctrl+K': { filter: o => true, emptyMessage: null, }, 'Apply': { filter: o => true, emptyMessage: null, }, + 'SCM': { filter: o => true, emptyMessage: null, }, } @@ -207,9 +208,9 @@ const _validatedModelState = (state: Omit): 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 } @@ -272,6 +273,12 @@ 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']) + } } catch (e) { readS = defaultState() diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index a911dfe6..25b3207e 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -350,7 +350,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 +365,9 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => { return 'Chat' else if (featureName === 'Apply') return 'Apply' + // source control: + else if (featureName === 'SCM') + return 'Source Control' else throw new Error(`Feature Name ${featureName} not allowed`) } diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCM.ts b/src/vs/workbench/contrib/void/electron-main/voidSCM.ts new file mode 100644 index 00000000..5d415623 --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/voidSCM.ts @@ -0,0 +1,56 @@ +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' +import { IVoidSCM } from '../common/voidSCM.js' +import { promisify } from 'util' +import { exec as _exec } from 'child_process' + +interface NumStat { + file: string + added: number + removed: number +} + +const exec = promisify(_exec) + +const git = async (command: string, path: string): Promise => { + const { stdout } = await exec(`${command}`, { cwd: path }) + //TODO VoidSCM - handle stderr + return stdout +} + +const getNumStat = async (path: string): Promise => { + const output = await git('git diff --numstat', 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): Promise => { + const diff = await git(`git diff --unified=0 --no-color -- "${file}"`, path) + return diff.slice(0, 2000) +} + +export class VoidSCM implements IVoidSCM { + readonly _serviceBrand: undefined + + async gitStat(path: string): Promise { + return await git('git diff --stat', path) + } + + async gitSampledDiffs(path: string): Promise { + const numStatList = await getNumStat(path) + const topFiles = numStatList + .sort((a, b) => (b.added + b.removed) - (a.added + a.removed)) + .slice(0, 10) + const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path) }))) + return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n') //TODO VoidSCM - investigate why file can be undefined + } +} + +registerSingleton(IVoidSCM, VoidSCM, InstantiationType.Delayed) From 4300df420ce22a70cb54ccf6e6fa4deaa805b558 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Mon, 12 May 2025 11:48:47 -0600 Subject: [PATCH 02/23] Refine VoidSCM TODO comments --- src/vs/workbench/contrib/void/browser/voidSCM.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCM.ts index 67bfe175..2fd90eeb 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCM.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCM.ts @@ -71,7 +71,7 @@ const onFinalMessage = (repo: ISCMRepository) => (params: { fullText: string }) } const sendLLMMessage = (messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions, repo: ISCMRepository, llmMessageService: ILLMMessageService) => { - //TODO VoidSCM - Experiment with messages to get better results + //TODO VoidSCM - Experiment with LLM messages to get better results. The results now seem decent. But it hasn't been tested much and could probably be improved. llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', messages, @@ -107,7 +107,7 @@ class CommitMessagePromptActionService extends Action2 { }) } - //TODO VoidSCM - handle loading state, debouncing, errors, and cancellations + //TODO VoidSCM - handle loading state, errors, aborting, and debouncing (possibly not needed) async run(accessor: ServicesAccessor): Promise { const scmService = accessor.get(ISCMService) const mainProcessService = accessor.get(IMainProcessService) From 327d2dfe06ec208f46a043d4013707c601f1f351 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Sun, 25 May 2025 09:16:19 -0400 Subject: [PATCH 03/23] Fix undefined files and propagate git errors --- .../contrib/void/electron-main/voidSCM.ts | 14 ++++++++------ 1 file changed, 8 insertions(+), 6 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCM.ts b/src/vs/workbench/contrib/void/electron-main/voidSCM.ts index 5d415623..c3261079 100644 --- a/src/vs/workbench/contrib/void/electron-main/voidSCM.ts +++ b/src/vs/workbench/contrib/void/electron-main/voidSCM.ts @@ -12,9 +12,11 @@ interface NumStat { const exec = promisify(_exec) const git = async (command: string, path: string): Promise => { - const { stdout } = await exec(`${command}`, { cwd: path }) - //TODO VoidSCM - handle stderr - return stdout + const { stdout, stderr } = await exec(`${command}`, { cwd: path }) + if (stderr) { + throw new Error(stderr) + } + return stdout.trim() } const getNumStat = async (path: string): Promise => { @@ -39,8 +41,8 @@ const getSampledDiff = async (file: string, path: string): Promise => { export class VoidSCM implements IVoidSCM { readonly _serviceBrand: undefined - async gitStat(path: string): Promise { - return await git('git diff --stat', path) + gitStat(path: string): Promise { + return git('git diff --stat', path) } async gitSampledDiffs(path: string): Promise { @@ -49,7 +51,7 @@ export class VoidSCM implements IVoidSCM { .sort((a, b) => (b.added + b.removed) - (a.added + a.removed)) .slice(0, 10) const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path) }))) - return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n') //TODO VoidSCM - investigate why file can be undefined + return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n') } } From 38f06202ed7e434e4c4c469c5486e7dcc9fddcd0 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Sun, 25 May 2025 09:16:46 -0400 Subject: [PATCH 04/23] Sync scm model to chat model --- .../workbench/contrib/void/common/voidSettingsService.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 093e40d9..f7d9bfad 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -256,6 +256,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()) @@ -391,6 +392,10 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { } + private _onUpdate_syncSCMToChat() { + this.setModelSelectionOfFeature('SCM', deepClone(this.state.modelSelectionOfFeature['Chat'])) + } + setGlobalSetting: SetGlobalSettingFn = async (settingName, newVal) => { const newState: VoidSettingsState = { ...this.state, @@ -405,6 +410,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // hooks if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat() + this._onUpdate_syncSCMToChat() } @@ -425,6 +431,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // hooks if (featureName === 'Chat') { if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat() + this._onUpdate_syncSCMToChat() } } From 033a23aa287d8c1d6e8f26407e3b00282481449f Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Sun, 25 May 2025 09:17:01 -0400 Subject: [PATCH 05/23] Update service name --- src/vs/workbench/contrib/void/common/voidSCM.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/voidSCM.ts b/src/vs/workbench/contrib/void/common/voidSCM.ts index da11dedf..3a4a5031 100644 --- a/src/vs/workbench/contrib/void/common/voidSCM.ts +++ b/src/vs/workbench/contrib/void/common/voidSCM.ts @@ -16,4 +16,4 @@ export interface IVoidSCM { gitSampledDiffs(path: string): Promise } -export const IVoidSCM = createDecorator('void-scm') +export const IVoidSCM = createDecorator('voidSCMService') From 3a3955120599d6bd52bbd2a16d2ea8c747eefa8b Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Sun, 25 May 2025 09:18:08 -0400 Subject: [PATCH 06/23] Handle loading, aborting, debouncing, and errors --- .../workbench/contrib/void/browser/voidSCM.ts | 312 +++++++++++++----- 1 file changed, 223 insertions(+), 89 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCM.ts index 2fd90eeb..703febcd 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCM.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCM.ts @@ -1,8 +1,7 @@ import { ThemeIcon } from '../../../../base/common/themables.js' import { localize2 } from '../../../../nls.js' import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/common/actions.js' -import { ContextKeyExpr } from '../../../../platform/contextkey/common/contextkey.js' -import { ServicesAccessor } from '../../../../platform/instantiation/common/instantiation.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 { IVoidSCM } from '../common/voidSCM.js' @@ -14,6 +13,13 @@ import { ModelSelection, OverridesOfModel, ModelSelectionOptions } from '../comm import { commitMessage_systemMessage } from '../common/prompt/prompts.js' import { LLMChatMessage } from '../common/sendLLMMessageTypes.js' import { ISCMRepository } from '../../../../workbench/contrib/scm/common/scm.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 @@ -21,111 +27,239 @@ interface ModelOptions { overridesOfModel: OverridesOfModel } -const scm = 'SCM' +export interface IGenerateCommitMessageService { + readonly _serviceBrand: undefined; + generateCommitMessage(): Promise + abort(): void +} -const prepareModelOptions = (settingsService: IVoidSettingsService): ModelOptions => { - const modelSelection = settingsService.state.modelSelectionOfFeature[scm] - const modelSelectionOptions = modelSelection ? settingsService.state.optionsOfModelSelection[scm][modelSelection?.providerName]?.[modelSelection.modelName] : undefined - const overridesOfModel = settingsService.state.overridesOfModel - return { - modelSelection, - modelSelectionOptions, - overridesOfModel +export const IGenerateCommitMessageService = createDecorator('voidGenerateCommitMessageService'); + +class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService { + readonly _serviceBrand: undefined; + private readonly scm = 'SCM' + private readonly execute = new ThrottledDelayer(300) + private llmRequestId: string | null = null + private currentRequestId: string | null = null + private voidSCM: IVoidSCM + private loadingContextKey: IContextKey + + 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('voidSCMGenerateCommitMessageLoading', false) + this.voidSCM = ProxyChannel.toService(mainProcessService.getChannel('void-channel-scm')); + } + + override dispose() { + this.execute.dispose() + super.dispose() + } + + async generateCommitMessage() { + this.setLoading(true) + this.execute.trigger(async () => { + const requestId = this.setRequestId() + try { + const { path, repo } = this.gitRepoInfo() + const [stat, sampledDiffs] = await Promise.all([this.voidSCM.gitStat(path), this.voidSCM.gitSampledDiffs(path)]) + this.checkIsCurrentRequest(requestId) + const modelOptions = this.prepareModelOptions() + const prompt = this.preparePrompt(stat, sampledDiffs) + const { messages, separateSystemMessage } = this.prepareMessages(prompt, modelOptions) + const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions) + this.checkIsCurrentRequest(requestId) + this.setCommitMessage(repo, commitMessage) + } catch (error) { + this.onError(error) + } finally { + if (this.isCurrentRequest(requestId)) { + this.setLoading(false) + } + } + }) + } + + abort() { + if (this.llmRequestId) { + this.llmMessageService.abort(this.llmRequestId) + } + this.execute.cancel() + this.setLoading(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 { + //TODO VoidSCM - Experiment with LLM messages to get better results. The results now seem decent. But it hasn't been tested much and could probably be improved. + return new Promise((resolve, reject) => { + const onFinalMessage = (params: { fullText: string }) => { + const match = params.fullText.match(/([\s\S]*?)<\/output>/i) + const commitMessage = match ? match[1].trim() : '' + resolve(commitMessage) + } + + const onError = (error: any) => { + console.error(error) + reject(error) + } + + const onAbort = () => { + reject(new CancellationError()) + } + + this.llmRequestId = this.llmMessageService.sendLLMMessage({ + messagesType: 'chatMessages', + messages, + separateSystemMessage, + chatMode: null, + modelSelection: modelOptions.modelSelection, + modelSelectionOptions: modelOptions.modelSelectionOptions, + overridesOfModel: modelOptions.overridesOfModel, + onText: () => { }, + onFinalMessage: onFinalMessage, + onError: onError, + onAbort: onAbort, + logging: { loggingName: 'VoidSCM - Commit Message' }, + }) + }) + } + + private prepareModelOptions(): ModelOptions { + const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[this.scm] + const modelSelectionOptions = modelSelection ? this.voidSettingsService.state.optionsOfModelSelection[this.scm][modelSelection?.providerName]?.[modelSelection.modelName] : undefined + const overridesOfModel = this.voidSettingsService.state.overridesOfModel + return { + modelSelection, + modelSelectionOptions, + overridesOfModel + } + } + + private preparePrompt(stat: string, sampledDiffs: string) { + const section1 = `Section 1 - Summary of Changes (git diff --stat):` + const section2 = `Section 2 - Sampled File Diffs (Top changed files):` + 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} + `.trim() + } + + private prepareMessages(prompt: string, modelOptions: ModelOptions) { + const simpleMessages = [{ role: 'user' as 'user', content: prompt }] + const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({ + simpleMessages, + systemMessage: commitMessage_systemMessage, + modelSelection: modelOptions.modelSelection, + featureName: this.scm, + }) + return { + messages, + separateSystemMessage + } + } + + /** Request Helpers */ + + private setRequestId() { + const requestId = generateUuid() + this.currentRequestId = requestId + return requestId + } + + private isCurrentRequest(requestId: string) { + return requestId === this.currentRequestId + } + + private checkIsCurrentRequest(requestId: string) { + if (!this.isCurrentRequest(requestId)) { + throw new CancellationError() + } + } + + /** UI Functions */ + + private setLoading(isLoading: boolean) { + this.loadingContextKey.set(isLoading) + } + + private setCommitMessage(repo: ISCMRepository, commitMessage: string) { + repo.input.setValue(commitMessage, false) + } + + private onError(error: any) { + if (!isCancellationError(error)) { + console.error(error) + this.notificationService.error('Failed to generate commit message') + } } } -const preparePrompt = (stat: string, sampledDiffs: string) => { - const section1 = `Section 1 - Summary of Changes (git diff --stat):` - const section2 = `Section 2 - Sampled File Diffs (Top changed files):` - 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} -`.trim() -} - -const prepareMessages = (prompt: string, modelOptions: ModelOptions, convertToLLMMessageService: IConvertToLLMMessageService) => { - const simpleMessages = [{ role: 'user' as 'user', content: prompt }] - const { messages, separateSystemMessage } = convertToLLMMessageService.prepareLLMSimpleMessages({ - simpleMessages, - systemMessage: commitMessage_systemMessage, - modelSelection: modelOptions.modelSelection, - featureName: scm, - }) - return { - messages, - separateSystemMessage - } -} - -const onFinalMessage = (repo: ISCMRepository) => (params: { fullText: string }) => { - const match = params.fullText.match(/([\s\S]*?)<\/output>/i) - const commitMessage = match ? match[1].trim() : '' - repo.input.setValue(commitMessage, false) -} - -const sendLLMMessage = (messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions, repo: ISCMRepository, llmMessageService: ILLMMessageService) => { - //TODO VoidSCM - Experiment with LLM messages to get better results. The results now seem decent. But it hasn't been tested much and could probably be improved. - llmMessageService.sendLLMMessage({ - messagesType: 'chatMessages', - messages, - separateSystemMessage, - chatMode: null, - modelSelection: modelOptions.modelSelection, - modelSelectionOptions: modelOptions.modelSelectionOptions, - overridesOfModel: modelOptions.overridesOfModel, - onText: () => { }, - onFinalMessage: onFinalMessage(repo), - onError: (error: any) => { console.error(error) }, //TODO VoidSCM - handle errors - onAbort: () => { console.log('abort') }, //TODO VoidSCM - handle abort - logging: { loggingName: 'VoidSCM - Commit Message' }, - }) -} - - -class CommitMessagePromptActionService extends Action2 { - private static readonly VOID_COMMIT_MESSAGE_PROMPT_ID = 'void.commitMessagePrompt' - +class GenerateCommitMessageAction extends Action2 { constructor() { super({ - id: CommitMessagePromptActionService.VOID_COMMIT_MESSAGE_PROMPT_ID, + 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.equals('scmProvider', 'git'), + when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals('voidSCMGenerateCommitMessageLoading', false)), group: 'inline' }] }) } - //TODO VoidSCM - handle loading state, errors, aborting, and debouncing (possibly not needed) async run(accessor: ServicesAccessor): Promise { - const scmService = accessor.get(ISCMService) - const mainProcessService = accessor.get(IMainProcessService) - const voidSettingsService = accessor.get(IVoidSettingsService) - const convertToLLMMessageService = accessor.get(IConvertToLLMMessageService) - const llmMessageService = accessor.get(ILLMMessageService) - const voidSCM = ProxyChannel.toService(mainProcessService.getChannel('void-channel-scm')) - - const repo = Array.from(scmService.repositories || []).find((r: any) => r.provider.contextValue === 'git') - //TODO VoidSCM - use the notification service to show an error if repo or rootUri is not found - if (!repo || !repo.provider.rootUri?.fsPath) { return } - const path = repo.provider.rootUri.fsPath - const [stat, sampledDiffs] = await Promise.all([voidSCM.gitStat(path), voidSCM.gitSampledDiffs(path)]) - const modelOptions = prepareModelOptions(voidSettingsService) - const prompt = preparePrompt(stat, sampledDiffs) - const { messages, separateSystemMessage } = prepareMessages(prompt, modelOptions, convertToLLMMessageService) - sendLLMMessage(messages, separateSystemMessage!, modelOptions, repo, llmMessageService) + const generateCommitMessageService = accessor.get(IGenerateCommitMessageService) + generateCommitMessageService.generateCommitMessage() } } -registerAction2(CommitMessagePromptActionService) +class LoadingGenerateCommitMessageAction extends Action2 { + constructor() { + super({ + id: 'void.loadingGenerateCommitMessageAction', + title: localize2('voidCommitMessagePromptCancel', 'Cancel'), + icon: ThemeIcon.fromId('stop-circle'), + tooltip: localize2('voidCommitMessagePromptCancelTooltip', 'Cancel'), + f1: true, + menu: [{ + id: MenuId.SCMInputBox, + when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals('voidSCMGenerateCommitMessageLoading', true)), + group: 'inline' + }] + }) + } + async run(accessor: ServicesAccessor): Promise { + const generateCommitMessageService = accessor.get(IGenerateCommitMessageService) + generateCommitMessageService.abort() + } +} + +registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed) +registerAction2(GenerateCommitMessageAction) +registerAction2(LoadingGenerateCommitMessageAction) From 5e100fcf1da0663ae94b8ef54de82194c91b7997 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Sun, 25 May 2025 22:01:41 -0400 Subject: [PATCH 07/23] Limit commit message length --- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 4071abcf..9979ae98 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -977,7 +977,7 @@ Store Result: After computing fib(n), the result is stored in memo for future re // ======================================================== scm ======================================================================== export const commitMessage_systemMessage = ` -You are an expert software engineer AI assistant responsible for writing clear, concise Git commit messages. +You are an expert software engineer AI assistant responsible for writing clear, concise Git commit messages. Keep your commit messages to 2 sentences or less. You always respond with: - The commit message wrapped in tags From f88dba76103b1aa85ed57be5bff5367aac909aba Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Mon, 26 May 2025 11:41:12 -0400 Subject: [PATCH 08/23] Add git branch and log info to prompt --- .../workbench/contrib/void/browser/voidSCM.ts | 32 +++++++++++++------ .../workbench/contrib/void/common/voidSCM.ts | 12 +++++++ .../contrib/void/electron-main/voidSCM.ts | 8 +++++ 3 files changed, 43 insertions(+), 9 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCM.ts index 703febcd..ed8f6328 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCM.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCM.ts @@ -69,10 +69,15 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit const requestId = this.setRequestId() try { const { path, repo } = this.gitRepoInfo() - const [stat, sampledDiffs] = await Promise.all([this.voidSCM.gitStat(path), this.voidSCM.gitSampledDiffs(path)]) + const [stat, sampledDiffs, branch, log] = await Promise.all([ + this.voidSCM.gitStat(path), + this.voidSCM.gitSampledDiffs(path), + this.voidSCM.gitBranch(path), + this.voidSCM.gitLog(path) + ]) this.checkIsCurrentRequest(requestId) const modelOptions = this.prepareModelOptions() - const prompt = this.preparePrompt(stat, sampledDiffs) + const prompt = this.preparePrompt(stat, sampledDiffs, branch, log) const { messages, separateSystemMessage } = this.prepareMessages(prompt, modelOptions) const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions) this.checkIsCurrentRequest(requestId) @@ -151,20 +156,29 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit } } - private preparePrompt(stat: string, sampledDiffs: string) { + private preparePrompt(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. +Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes. - ${section1} +${section1} - ${stat} +${stat} - ${section2} +${section2} - ${sampledDiffs} - `.trim() +${sampledDiffs} + +${section3} + +${branch} + +${section4} + +${log}`.trim() } private prepareMessages(prompt: string, modelOptions: ModelOptions) { diff --git a/src/vs/workbench/contrib/void/common/voidSCM.ts b/src/vs/workbench/contrib/void/common/voidSCM.ts index 3a4a5031..3571fb3d 100644 --- a/src/vs/workbench/contrib/void/common/voidSCM.ts +++ b/src/vs/workbench/contrib/void/common/voidSCM.ts @@ -14,6 +14,18 @@ export interface IVoidSCM { * @param path Path to the git repository */ gitSampledDiffs(path: string): Promise + /** + * Get the current git branch + * + * @param path Path to the git repository + */ + gitBranch(path: string): Promise + /** + * Get the last 5 commits excluding merges + * + * @param path Path to the git repository + */ + gitLog(path: string): Promise } export const IVoidSCM = createDecorator('voidSCMService') diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCM.ts b/src/vs/workbench/contrib/void/electron-main/voidSCM.ts index c3261079..9f8d211e 100644 --- a/src/vs/workbench/contrib/void/electron-main/voidSCM.ts +++ b/src/vs/workbench/contrib/void/electron-main/voidSCM.ts @@ -53,6 +53,14 @@ export class VoidSCM implements IVoidSCM { const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path) }))) return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n') } + + gitBranch(path: string): Promise { + return git('git branch --show-current', path) + } + + gitLog(path: string): Promise { + return git('git log --pretty=format:"%h|%s|%ad" --date=short --no-merges -n 5', path) + } } registerSingleton(IVoidSCM, VoidSCM, InstantiationType.Delayed) From 189e207499fc84de991b69f062091083b0ba8da6 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Mon, 26 May 2025 11:41:45 -0400 Subject: [PATCH 09/23] Improve system message prompt --- src/vs/workbench/contrib/void/common/prompt/prompts.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 9979ae98..d9d5e5c7 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -977,7 +977,7 @@ Store Result: After computing fib(n), the result is stored in memo for future re // ======================================================== scm ======================================================================== export const commitMessage_systemMessage = ` -You are an expert software engineer AI assistant responsible for writing clear, concise Git commit messages. Keep your commit messages to 2 sentences or less. +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 tags From f74b94e3ba6e19cdc800741d47ef1d963ef22063 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Mon, 26 May 2025 13:04:23 -0400 Subject: [PATCH 10/23] Refactor: Move and rename prompt generation logic for commit messages --- .../contrib/void/common/prompt/prompts.ts | 59 ++++++++++++++++++- 1 file changed, 58 insertions(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index d9d5e5c7..8d1a500b 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -976,7 +976,7 @@ Store Result: After computing fib(n), the result is stored in memo for future re // ======================================================== scm ======================================================================== -export const commitMessage_systemMessage = ` +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: @@ -989,3 +989,60 @@ Example format: Do not include anything else outside of these tags. Never include quotes, markdown, commentary, or explanations outside of and .`.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() +} From f034be08c311d0273e95849b8e2bfe70c10e538d Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Mon, 26 May 2025 13:04:41 -0400 Subject: [PATCH 11/23] Refactor: Update commit message generation to use new prompt structure and system messages --- .../workbench/contrib/void/browser/voidSCM.ts | 48 +++++-------------- 1 file changed, 13 insertions(+), 35 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCM.ts index ed8f6328..6b527408 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCM.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCM.ts @@ -10,7 +10,7 @@ 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 { commitMessage_systemMessage } from '../common/prompt/prompts.js' +import { gitCommitMessage_systemMessage, gitCommitMessage_userMessage } from '../common/prompt/prompts.js' import { LLMChatMessage } from '../common/sendLLMMessageTypes.js' import { ISCMRepository } from '../../../../workbench/contrib/scm/common/scm.js' import { generateUuid } from '../../../../base/common/uuid.js' @@ -28,13 +28,15 @@ interface ModelOptions { } export interface IGenerateCommitMessageService { - readonly _serviceBrand: undefined; + readonly _serviceBrand: undefined generateCommitMessage(): Promise abort(): void } export const IGenerateCommitMessageService = createDecorator('voidGenerateCommitMessageService'); +const loadingContextKey = 'voidSCMGenerateCommitMessageLoading' + class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService { readonly _serviceBrand: undefined; private readonly scm = 'SCM' @@ -54,7 +56,7 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit @INotificationService private readonly notificationService: INotificationService ) { super() - this.loadingContextKey = this.contextKeyService.createKey('voidSCMGenerateCommitMessageLoading', false) + this.loadingContextKey = this.contextKeyService.createKey(loadingContextKey, false) this.voidSCM = ProxyChannel.toService(mainProcessService.getChannel('void-channel-scm')); } @@ -77,7 +79,8 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit ]) this.checkIsCurrentRequest(requestId) const modelOptions = this.prepareModelOptions() - const prompt = this.preparePrompt(stat, sampledDiffs, branch, log) + const prompt = gitCommitMessage_userMessage(stat, sampledDiffs, branch, log) + console.log(prompt) const { messages, separateSystemMessage } = this.prepareMessages(prompt, modelOptions) const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions) this.checkIsCurrentRequest(requestId) @@ -156,36 +159,11 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit } } - private preparePrompt(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() - } - private prepareMessages(prompt: string, modelOptions: ModelOptions) { const simpleMessages = [{ role: 'user' as 'user', content: prompt }] const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({ simpleMessages, - systemMessage: commitMessage_systemMessage, + systemMessage: gitCommitMessage_systemMessage, modelSelection: modelOptions.modelSelection, featureName: this.scm, }) @@ -241,7 +219,7 @@ class GenerateCommitMessageAction extends Action2 { f1: true, menu: [{ id: MenuId.SCMInputBox, - when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals('voidSCMGenerateCommitMessageLoading', false)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, false)), group: 'inline' }] }) @@ -257,13 +235,13 @@ class LoadingGenerateCommitMessageAction extends Action2 { constructor() { super({ id: 'void.loadingGenerateCommitMessageAction', - title: localize2('voidCommitMessagePromptCancel', 'Cancel'), + title: localize2('voidCommitMessagePromptCancel', 'Void: Cancel Commit Message Generation'), icon: ThemeIcon.fromId('stop-circle'), - tooltip: localize2('voidCommitMessagePromptCancelTooltip', 'Cancel'), - f1: true, + 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('voidSCMGenerateCommitMessageLoading', true)), + when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, true)), group: 'inline' }] }) From 06d20d705ebfbc9490390c890ebef174e4a567d1 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Mon, 26 May 2025 18:07:40 -0400 Subject: [PATCH 12/23] Remove log --- src/vs/workbench/contrib/void/browser/voidSCM.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCM.ts index 6b527408..0a0b3196 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCM.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCM.ts @@ -80,7 +80,6 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit this.checkIsCurrentRequest(requestId) const modelOptions = this.prepareModelOptions() const prompt = gitCommitMessage_userMessage(stat, sampledDiffs, branch, log) - console.log(prompt) const { messages, separateSystemMessage } = this.prepareMessages(prompt, modelOptions) const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions) this.checkIsCurrentRequest(requestId) From e2863d3002e8cc08d27936aca56b8074bc8467d0 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 22:20:16 -0700 Subject: [PATCH 13/23] simplify --- .../workbench/contrib/void/browser/voidSCM.ts | 105 +++++++----------- 1 file changed, 40 insertions(+), 65 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCM.ts index 0a0b3196..3a2170c7 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCM.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCM.ts @@ -39,7 +39,6 @@ const loadingContextKey = 'voidSCMGenerateCommitMessageLoading' class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService { readonly _serviceBrand: undefined; - private readonly scm = 'SCM' private readonly execute = new ThrottledDelayer(300) private llmRequestId: string | null = null private currentRequestId: string | null = null @@ -66,9 +65,12 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit } async generateCommitMessage() { - this.setLoading(true) + this.loadingContextKey.set(true) this.execute.trigger(async () => { - const requestId = this.setRequestId() + const requestId = generateUuid() + this.currentRequestId = requestId + + try { const { path, repo } = this.gitRepoInfo() const [stat, sampledDiffs, branch, log] = await Promise.all([ @@ -77,18 +79,35 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit this.voidSCM.gitBranch(path), this.voidSCM.gitLog(path) ]) - this.checkIsCurrentRequest(requestId) - const modelOptions = this.prepareModelOptions() + + 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 { messages, separateSystemMessage } = this.prepareMessages(prompt, modelOptions) + + 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) - this.checkIsCurrentRequest(requestId) + + if (!this.isCurrentRequest(requestId)) { throw new CancellationError() } + this.setCommitMessage(repo, commitMessage) } catch (error) { this.onError(error) } finally { if (this.isCurrentRequest(requestId)) { - this.setLoading(false) + this.loadingContextKey.set(false) } } }) @@ -99,7 +118,7 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit this.llmMessageService.abort(this.llmRequestId) } this.execute.cancel() - this.setLoading(false) + this.loadingContextKey.set(false) this.currentRequestId = null } @@ -115,20 +134,6 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit private sendLLMMessage(messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions): Promise { //TODO VoidSCM - Experiment with LLM messages to get better results. The results now seem decent. But it hasn't been tested much and could probably be improved. return new Promise((resolve, reject) => { - const onFinalMessage = (params: { fullText: string }) => { - const match = params.fullText.match(/([\s\S]*?)<\/output>/i) - const commitMessage = match ? match[1].trim() : '' - resolve(commitMessage) - } - - const onError = (error: any) => { - console.error(error) - reject(error) - } - - const onAbort = () => { - reject(new CancellationError()) - } this.llmRequestId = this.llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', @@ -139,63 +144,33 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit modelSelectionOptions: modelOptions.modelSelectionOptions, overridesOfModel: modelOptions.overridesOfModel, onText: () => { }, - onFinalMessage: onFinalMessage, - onError: onError, - onAbort: onAbort, + onFinalMessage: (params: { fullText: string }) => { + const match = params.fullText.match(/([\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' }, }) }) } - private prepareModelOptions(): ModelOptions { - const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[this.scm] - const modelSelectionOptions = modelSelection ? this.voidSettingsService.state.optionsOfModelSelection[this.scm][modelSelection?.providerName]?.[modelSelection.modelName] : undefined - const overridesOfModel = this.voidSettingsService.state.overridesOfModel - return { - modelSelection, - modelSelectionOptions, - overridesOfModel - } - } - - private prepareMessages(prompt: string, modelOptions: ModelOptions) { - const simpleMessages = [{ role: 'user' as 'user', content: prompt }] - const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({ - simpleMessages, - systemMessage: gitCommitMessage_systemMessage, - modelSelection: modelOptions.modelSelection, - featureName: this.scm, - }) - return { - messages, - separateSystemMessage - } - } /** Request Helpers */ - private setRequestId() { - const requestId = generateUuid() - this.currentRequestId = requestId - return requestId - } - private isCurrentRequest(requestId: string) { return requestId === this.currentRequestId } - private checkIsCurrentRequest(requestId: string) { - if (!this.isCurrentRequest(requestId)) { - throw new CancellationError() - } - } /** UI Functions */ - private setLoading(isLoading: boolean) { - this.loadingContextKey.set(isLoading) - } - private setCommitMessage(repo: ISCMRepository, commitMessage: string) { repo.input.setValue(commitMessage, false) } From b50079709d4e85a7ade78544784dafa8ed8003c0 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 22:32:11 -0700 Subject: [PATCH 14/23] rename scm to match void convention --- src/vs/code/electron-main/app.ts | 8 ++++---- .../contrib/void/browser/void.contribution.ts | 2 +- .../void/browser/{voidSCM.ts => voidSCMService.ts} | 10 ++++++---- .../void/common/{voidSCM.ts => voidSCMTypes.ts} | 4 ++-- .../void/electron-main/{voidSCM.ts => voidSCMMain.ts} | 6 +++--- 5 files changed, 16 insertions(+), 14 deletions(-) rename src/vs/workbench/contrib/void/browser/{voidSCM.ts => voidSCMService.ts} (96%) rename src/vs/workbench/contrib/void/common/{voidSCM.ts => voidSCMTypes.ts} (86%) rename src/vs/workbench/contrib/void/electron-main/{voidSCM.ts => voidSCMMain.ts} (90%) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 5406d2a8..b23d5425 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -130,8 +130,8 @@ 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 { IVoidSCM } from '../../workbench/contrib/void/common/voidSCM.js'; -import { VoidSCM } from '../../workbench/contrib/void/electron-main/voidSCM.js'; +import { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMain.js'; +import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.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,7 +1103,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(IVoidSCM, new SyncDescriptor(VoidSCM, undefined, false)); + services.set(IVoidSCMService, new SyncDescriptor(VoidSCMService, undefined, false)); // Default Extensions Profile Init services.set(IExtensionsProfileScannerService, new SyncDescriptor(ExtensionsProfileScannerService, undefined, true)); @@ -1245,7 +1245,7 @@ export class CodeApplication extends Disposable { const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel); - const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCM), disposables); + const voidSCMChannel = ProxyChannel.fromService(accessor.get(IVoidSCMService), disposables); mainProcessElectronServer.registerChannel('void-channel-scm', voidSCMChannel); // Extension Host Debug Broadcasting diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 27bc6701..35c89184 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -62,7 +62,7 @@ import './miscWokrbenchContrib.js' import './fileService.js' // register source control management -import './voidSCM.js' +import './voidSCMService.js' // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- diff --git a/src/vs/workbench/contrib/void/browser/voidSCM.ts b/src/vs/workbench/contrib/void/browser/voidSCMService.ts similarity index 96% rename from src/vs/workbench/contrib/void/browser/voidSCM.ts rename to src/vs/workbench/contrib/void/browser/voidSCMService.ts index 3a2170c7..b5dd85b8 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCM.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCMService.ts @@ -4,7 +4,7 @@ import { Action2, MenuId, registerAction2 } from '../../../../platform/actions/c 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 { IVoidSCM } from '../common/voidSCM.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' @@ -12,7 +12,6 @@ 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 { ISCMRepository } from '../../../../workbench/contrib/scm/common/scm.js' import { generateUuid } from '../../../../base/common/uuid.js' import { ThrottledDelayer } from '../../../../base/common/async.js' import { CancellationError, isCancellationError } from '../../../../base/common/errors.js' @@ -21,6 +20,9 @@ import { createDecorator, ServicesAccessor } from '../../../../platform/instanti import { Disposable } from '../../../../base/common/lifecycle.js' import { INotificationService } from '../../../../platform/notification/common/notification.js' +// this is OK, it's just a type +import type { ISCMRepository } from '../../scm/common/scm.js' + interface ModelOptions { modelSelection: ModelSelection | null modelSelectionOptions?: ModelSelectionOptions @@ -42,7 +44,7 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit private readonly execute = new ThrottledDelayer(300) private llmRequestId: string | null = null private currentRequestId: string | null = null - private voidSCM: IVoidSCM + private voidSCM: IVoidSCMService private loadingContextKey: IContextKey constructor( @@ -56,7 +58,7 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit ) { super() this.loadingContextKey = this.contextKeyService.createKey(loadingContextKey, false) - this.voidSCM = ProxyChannel.toService(mainProcessService.getChannel('void-channel-scm')); + this.voidSCM = ProxyChannel.toService(mainProcessService.getChannel('void-channel-scm')); } override dispose() { diff --git a/src/vs/workbench/contrib/void/common/voidSCM.ts b/src/vs/workbench/contrib/void/common/voidSCMTypes.ts similarity index 86% rename from src/vs/workbench/contrib/void/common/voidSCM.ts rename to src/vs/workbench/contrib/void/common/voidSCMTypes.ts index 3571fb3d..8aaba15c 100644 --- a/src/vs/workbench/contrib/void/common/voidSCM.ts +++ b/src/vs/workbench/contrib/void/common/voidSCMTypes.ts @@ -1,6 +1,6 @@ import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; -export interface IVoidSCM { +export interface IVoidSCMService { readonly _serviceBrand: undefined; /** * Get git diff --stat @@ -28,4 +28,4 @@ export interface IVoidSCM { gitLog(path: string): Promise } -export const IVoidSCM = createDecorator('voidSCMService') +export const IVoidSCMService = createDecorator('voidSCMService') diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCM.ts b/src/vs/workbench/contrib/void/electron-main/voidSCMMain.ts similarity index 90% rename from src/vs/workbench/contrib/void/electron-main/voidSCM.ts rename to src/vs/workbench/contrib/void/electron-main/voidSCMMain.ts index 9f8d211e..5294bc23 100644 --- a/src/vs/workbench/contrib/void/electron-main/voidSCM.ts +++ b/src/vs/workbench/contrib/void/electron-main/voidSCMMain.ts @@ -1,5 +1,5 @@ import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' -import { IVoidSCM } from '../common/voidSCM.js' +import { IVoidSCMService } from '../common/voidSCMTypes.js' import { promisify } from 'util' import { exec as _exec } from 'child_process' @@ -38,7 +38,7 @@ const getSampledDiff = async (file: string, path: string): Promise => { return diff.slice(0, 2000) } -export class VoidSCM implements IVoidSCM { +export class VoidSCMService implements IVoidSCMService { readonly _serviceBrand: undefined gitStat(path: string): Promise { @@ -63,4 +63,4 @@ export class VoidSCM implements IVoidSCM { } } -registerSingleton(IVoidSCM, VoidSCM, InstantiationType.Delayed) +registerSingleton(IVoidSCMService, VoidSCMService, InstantiationType.Delayed) From 22e08e6a064aadee337708416d8c9ea98cf4175b Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 22:32:44 -0700 Subject: [PATCH 15/23] rename --- src/vs/code/electron-main/app.ts | 2 +- .../electron-main/{voidSCMMain.ts => voidSCMMainService.ts} | 0 2 files changed, 1 insertion(+), 1 deletion(-) rename src/vs/workbench/contrib/void/electron-main/{voidSCMMain.ts => voidSCMMainService.ts} (100%) diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index b23d5425..13ec219b 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -130,7 +130,7 @@ 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/voidSCMMain.js'; +import { VoidSCMService } from '../../workbench/contrib/void/electron-main/voidSCMMainService.js'; import { IVoidSCMService } from '../../workbench/contrib/void/common/voidSCMTypes.js'; /** * The main VS Code application. There will only ever be one instance, diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCMMain.ts b/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts similarity index 100% rename from src/vs/workbench/contrib/void/electron-main/voidSCMMain.ts rename to src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts From 8350fbd2d11782c0517abd42d060f04c38eeba39 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 22:36:36 -0700 Subject: [PATCH 16/23] Include both staged and unstaged changes in git diff statistics --- .../void/electron-main/voidSCMMainService.ts | 93 ++++++++++++++++--- 1 file changed, 78 insertions(+), 15 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts b/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts index 5294bc23..56f0a26e 100644 --- a/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts +++ b/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts @@ -20,29 +20,92 @@ const git = async (command: string, path: string): Promise => { } const getNumStat = async (path: string): Promise => { - const output = await git('git diff --numstat', 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, - } - }) + // Get both staged and unstaged changes + const [stagedOutput, unstagedOutput] = await Promise.all([ + git('git diff --cached --numstat', path).catch(() => ''), // staged changes + git('git diff --numstat', path).catch(() => '') // unstaged changes + ]) + + const parseOutput = (output: string) => { + if (!output.trim()) return [] + return output + .split('\n') + .filter(line => line.trim()) + .map((line) => { + const [added, removed, file] = line.split('\t') + return { + file, + added: parseInt(added, 10) || 0, + removed: parseInt(removed, 10) || 0, + } + }) + } + + const stagedStats = parseOutput(stagedOutput) + const unstagedStats = parseOutput(unstagedOutput) + + // Combine and deduplicate by file, summing the changes + const fileMap = new Map() + + for (const stat of [...stagedStats, ...unstagedStats]) { + const existing = fileMap.get(stat.file) + if (existing) { + existing.added += stat.added + existing.removed += stat.removed + } else { + fileMap.set(stat.file, { ...stat }) + } + } + + return Array.from(fileMap.values()) } const getSampledDiff = async (file: string, path: string): Promise => { - const diff = await git(`git diff --unified=0 --no-color -- "${file}"`, path) - return diff.slice(0, 2000) + // Get both staged and unstaged diffs + const [stagedDiff, unstagedDiff] = await Promise.all([ + git(`git diff --cached --unified=0 --no-color -- "${file}"`, path).catch(() => ''), // staged changes + git(`git diff --unified=0 --no-color -- "${file}"`, path).catch(() => '') // unstaged changes + ]) + + let combinedDiff = '' + if (stagedDiff.trim()) { + combinedDiff += `=== STAGED CHANGES ===\n${stagedDiff}\n\n` + } + if (unstagedDiff.trim()) { + combinedDiff += `=== UNSTAGED CHANGES ===\n${unstagedDiff}\n\n` + } + + return combinedDiff.slice(0, 2000) } export class VoidSCMService implements IVoidSCMService { readonly _serviceBrand: undefined - gitStat(path: string): Promise { - return git('git diff --stat', path) + async gitStat(path: string): Promise { + // Get both staged and unstaged stats + const [stagedStat, unstagedStat] = await Promise.all([ + git('git diff --cached --stat', path).catch(() => ''), // staged changes + git('git diff --stat', path).catch(() => '') // unstaged changes + ]) + + let combinedStat = '' + if (stagedStat.trim()) { + combinedStat += `Staged changes:\n${stagedStat}\n\n` + } + if (unstagedStat.trim()) { + combinedStat += `Unstaged changes:\n${unstagedStat}\n\n` + } + + // If neither staged nor unstaged changes, check if there are any changes at all + if (!combinedStat.trim()) { + // This will show changes between HEAD and working directory (includes staged changes) + const allChanges = await git('git diff HEAD --stat', path).catch(() => '') + if (allChanges.trim()) { + combinedStat = `All changes:\n${allChanges}` + } + } + + return combinedStat.trim() } async gitSampledDiffs(path: string): Promise { From bd1320851342a242ef3d7fe16300eee0f2d7c7f4 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 29 May 2025 22:37:59 -0700 Subject: [PATCH 17/23] rm todo --- src/vs/workbench/contrib/void/browser/voidSCMService.ts | 1 - 1 file changed, 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCMService.ts b/src/vs/workbench/contrib/void/browser/voidSCMService.ts index b5dd85b8..ab542928 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCMService.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCMService.ts @@ -134,7 +134,6 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit /** LLM Functions */ private sendLLMMessage(messages: LLMChatMessage[], separateSystemMessage: string, modelOptions: ModelOptions): Promise { - //TODO VoidSCM - Experiment with LLM messages to get better results. The results now seem decent. But it hasn't been tested much and could probably be improved. return new Promise((resolve, reject) => { this.llmRequestId = this.llmMessageService.sendLLMMessage({ From 31950ae01e635a668a4178f1cb7fbf65a2a0c241 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 30 May 2025 01:24:14 -0700 Subject: [PATCH 18/23] sync scm --- .../react/src/void-settings-tsx/Settings.tsx | 23 +++++++++++++++++++ .../contrib/void/common/voidSettingsTypes.ts | 2 ++ 2 files changed, 25 insertions(+) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 52e00dc2..112a5330 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1218,9 +1218,32 @@ export const Settings = () => { + {/* SCM */} + +
+

{displayInfoOfFeatureName('SCM')}

+
Settings that control the behavior of Source Control features.
+
+ {/* Sync to Chat Switch */} +
+ voidSettingsService.setGlobalSetting('syncSCMToChat', newVal)} + /> + {settingsState.globalSettings.syncSCMToChat ? 'Same as Chat model' : 'Different model'} +
+ {/* Model Dropdown */} +
+ +
+
+ +
+
{/* Tools Section */}
diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index afb0f529..443e02a6 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -446,6 +446,7 @@ export type GlobalSettings = { aiInstructions: string; enableAutocomplete: boolean; syncApplyToChat: boolean; + syncSCMToChat: boolean; enableFastApply: boolean; chatMode: ChatMode; autoApprove: { [approvalType in ToolApprovalType]?: boolean }; @@ -460,6 +461,7 @@ export const defaultGlobalSettings: GlobalSettings = { aiInstructions: '', enableAutocomplete: false, syncApplyToChat: true, + syncSCMToChat: true, enableFastApply: true, chatMode: 'agent', autoApprove: {}, From 22fa0939287f96737362711c0c48496d5baf75dd Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 30 May 2025 01:26:34 -0700 Subject: [PATCH 19/23] sync --- .../void/browser/react/src/void-settings-tsx/Settings.tsx | 2 +- src/vs/workbench/contrib/void/common/voidSettingsTypes.ts | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 112a5330..e086a2ec 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1223,7 +1223,7 @@ export const Settings = () => {

{displayInfoOfFeatureName('SCM')}

-
Settings that control the behavior of Source Control features.
+
Settings that control the behavior of the Commit Message Generator.
{/* Sync to Chat Switch */} diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 443e02a6..90ac77eb 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -382,7 +382,7 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => { return 'Apply' // source control: else if (featureName === 'SCM') - return 'Source Control' + return 'Commit Message Generator' else throw new Error(`Feature Name ${featureName} not allowed`) } From 844784445c5b6209ac79fc10cb4e89345be94099 Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Thu, 5 Jun 2025 09:35:26 -0400 Subject: [PATCH 20/23] Localize commit message generation error notification --- src/vs/workbench/contrib/void/browser/voidSCMService.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/workbench/contrib/void/browser/voidSCMService.ts b/src/vs/workbench/contrib/void/browser/voidSCMService.ts index ab542928..dfe49e1d 100644 --- a/src/vs/workbench/contrib/void/browser/voidSCMService.ts +++ b/src/vs/workbench/contrib/void/browser/voidSCMService.ts @@ -179,7 +179,7 @@ class GenerateCommitMessageService extends Disposable implements IGenerateCommit private onError(error: any) { if (!isCancellationError(error)) { console.error(error) - this.notificationService.error('Failed to generate commit message') + this.notificationService.error(localize2('voidFailedToGenerateCommitMessage', 'Failed to generate commit message.').value) } } } From 9e53dfef3378412036fe886b34fc06e5d4cafc1d Mon Sep 17 00:00:00 2001 From: Steven Wexler Date: Thu, 5 Jun 2025 09:56:07 -0400 Subject: [PATCH 21/23] Support staged vs. unstaged changs. Increase diff size to 8000. --- .../void/electron-main/voidSCMMainService.ts | 117 +++++------------- 1 file changed, 34 insertions(+), 83 deletions(-) diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts b/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts index 56f0a26e..c32fcb08 100644 --- a/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts +++ b/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts @@ -1,7 +1,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' -import { IVoidSCMService } from '../common/voidSCMTypes.js' import { promisify } from 'util' import { exec as _exec } from 'child_process' +import { IVoidSCMService } from '../common/voidSCMTypes.js' interface NumStat { file: string @@ -11,6 +11,10 @@ interface NumStat { 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 => { const { stdout, stderr } = await exec(`${command}`, { cwd: path }) if (stderr) { @@ -19,101 +23,48 @@ const git = async (command: string, path: string): Promise => { return stdout.trim() } -const getNumStat = async (path: string): Promise => { - // Get both staged and unstaged changes - const [stagedOutput, unstagedOutput] = await Promise.all([ - git('git diff --cached --numstat', path).catch(() => ''), // staged changes - git('git diff --numstat', path).catch(() => '') // unstaged changes - ]) - - const parseOutput = (output: string) => { - if (!output.trim()) return [] - return output - .split('\n') - .filter(line => line.trim()) - .map((line) => { - const [added, removed, file] = line.split('\t') - return { - file, - added: parseInt(added, 10) || 0, - removed: parseInt(removed, 10) || 0, - } - }) - } - - const stagedStats = parseOutput(stagedOutput) - const unstagedStats = parseOutput(unstagedOutput) - - // Combine and deduplicate by file, summing the changes - const fileMap = new Map() - - for (const stat of [...stagedStats, ...unstagedStats]) { - const existing = fileMap.get(stat.file) - if (existing) { - existing.added += stat.added - existing.removed += stat.removed - } else { - fileMap.set(stat.file, { ...stat }) - } - } - - return Array.from(fileMap.values()) +const getNumStat = async (path: string, useStagedChanges: boolean): Promise => { + 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): Promise => { - // Get both staged and unstaged diffs - const [stagedDiff, unstagedDiff] = await Promise.all([ - git(`git diff --cached --unified=0 --no-color -- "${file}"`, path).catch(() => ''), // staged changes - git(`git diff --unified=0 --no-color -- "${file}"`, path).catch(() => '') // unstaged changes - ]) - - let combinedDiff = '' - if (stagedDiff.trim()) { - combinedDiff += `=== STAGED CHANGES ===\n${stagedDiff}\n\n` - } - if (unstagedDiff.trim()) { - combinedDiff += `=== UNSTAGED CHANGES ===\n${unstagedDiff}\n\n` - } - - return combinedDiff.slice(0, 2000) +const getSampledDiff = async (file: string, path: string, useStagedChanges: boolean): Promise => { + 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 => { + 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 { - // Get both staged and unstaged stats - const [stagedStat, unstagedStat] = await Promise.all([ - git('git diff --cached --stat', path).catch(() => ''), // staged changes - git('git diff --stat', path).catch(() => '') // unstaged changes - ]) - - let combinedStat = '' - if (stagedStat.trim()) { - combinedStat += `Staged changes:\n${stagedStat}\n\n` - } - if (unstagedStat.trim()) { - combinedStat += `Unstaged changes:\n${unstagedStat}\n\n` - } - - // If neither staged nor unstaged changes, check if there are any changes at all - if (!combinedStat.trim()) { - // This will show changes between HEAD and working directory (includes staged changes) - const allChanges = await git('git diff HEAD --stat', path).catch(() => '') - if (allChanges.trim()) { - combinedStat = `All changes:\n${allChanges}` - } - } - - return combinedStat.trim() + const useStagedChanges = await hasStagedChanges(path) + const staged = useStagedChanges ? '--staged' : '' + return git(`git diff --stat ${staged}`, path) } async gitSampledDiffs(path: string): Promise { - const numStatList = await getNumStat(path) + 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, 10) - const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path) }))) + .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') } From c41838afc69084c5485f5c461797584bae5206b7 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 5 Jun 2025 16:06:13 -0700 Subject: [PATCH 22/23] git commit --- .../react/src/void-settings-tsx/Settings.tsx | 27 +++++++++++++++++++ 1 file changed, 27 insertions(+) diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index b24efaed..429af0e1 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1342,6 +1342,33 @@ export const Settings = () => {
+ + {/* SCM */} + + +
+

{displayInfoOfFeatureName('SCM')}

+
Settings that control the behavior of the commit message generator.
+ +
+ {/* Sync to Chat Switch */} +
+ voidSettingsService.setGlobalSetting('syncSCMToChat', newVal)} + /> + {settingsState.globalSettings.syncSCMToChat ? 'Same as Chat model' : 'Different model'} +
+ + {/* Model Dropdown */} +
+ +
+
+ +
+
From 9a5580d32e0b9bd4266cdd675d9f3b80fa9c5cf2 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Thu, 5 Jun 2025 16:14:40 -0700 Subject: [PATCH 23/23] sync SCM --- .../workbench/contrib/void/common/voidSettingsService.ts | 7 ++++--- 1 file changed, 4 insertions(+), 3 deletions(-) diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 836fd687..dbd3cf3f 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -400,7 +400,6 @@ 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() { @@ -421,7 +420,8 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService { // hooks if (this.state.globalSettings.syncApplyToChat) this._onUpdate_syncApplyToChat() - this._onUpdate_syncSCMToChat() + if (this.state.globalSettings.syncSCMToChat) this._onUpdate_syncSCMToChat() + } @@ -441,7 +441,8 @@ 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() } }