diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index 1c103b1f..c3d2dfe5 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -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); 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 9ac3645c..a27cbf76 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/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 60294f06..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 @@ -688,6 +688,7 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam // // } + export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => { const voidSettingsState = useSettingsState() @@ -1341,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 */} +
+ +
+
+ +
+
diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index 3ab89abf..35c89184 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -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 diff --git a/src/vs/workbench/contrib/void/browser/voidSCMService.ts b/src/vs/workbench/contrib/void/browser/voidSCMService.ts new file mode 100644 index 00000000..dfe49e1d --- /dev/null +++ b/src/vs/workbench/contrib/void/browser/voidSCMService.ts @@ -0,0 +1,232 @@ +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' + +// this is OK, it's just a type +import type { ISCMRepository } from '../../scm/common/scm.js' + +interface ModelOptions { + modelSelection: ModelSelection | null + modelSelectionOptions?: ModelSelectionOptions + overridesOfModel: OverridesOfModel +} + +export interface IGenerateCommitMessageService { + 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 execute = new ThrottledDelayer(300) + private llmRequestId: string | null = null + private currentRequestId: string | null = null + private voidSCM: IVoidSCMService + 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(loadingContextKey, false) + this.voidSCM = ProxyChannel.toService(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() } + + this.setCommitMessage(repo, commitMessage) + } 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 { + 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(/([\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 setCommitMessage(repo: ISCMRepository, commitMessage: string) { + repo.input.setValue(commitMessage, false) + } + + 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 { + 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 { + const generateCommitMessageService = accessor.get(IGenerateCommitMessageService) + generateCommitMessageService.abort() + } +} + +registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed) +registerAction2(GenerateCommitMessageAction) +registerAction2(LoadingGenerateCommitMessageAction) diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 9e9c3b57..55412a80 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -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 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() + + +/** + * 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() +} diff --git a/src/vs/workbench/contrib/void/common/voidSCMTypes.ts b/src/vs/workbench/contrib/void/common/voidSCMTypes.ts new file mode 100644 index 00000000..8aaba15c --- /dev/null +++ b/src/vs/workbench/contrib/void/common/voidSCMTypes.ts @@ -0,0 +1,31 @@ +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 + /** + * 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 + /** + * 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 IVoidSCMService = createDecorator('voidSCMService') diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index e448627a..dbd3cf3f 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -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): 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,6 +282,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']) + } + // add disableSystemMessage feature if (readS.globalSettings.disableSystemMessage === undefined) readS.globalSettings.disableSystemMessage = false; } catch (e) { @@ -392,7 +400,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 +420,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 +441,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() } } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index 1731fdf8..90ac77eb 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -365,7 +365,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 @@ -380,6 +380,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`) } @@ -443,6 +446,7 @@ export type GlobalSettings = { aiInstructions: string; enableAutocomplete: boolean; syncApplyToChat: boolean; + syncSCMToChat: boolean; enableFastApply: boolean; chatMode: ChatMode; autoApprove: { [approvalType in ToolApprovalType]?: boolean }; @@ -457,6 +461,7 @@ export const defaultGlobalSettings: GlobalSettings = { aiInstructions: '', enableAutocomplete: false, syncApplyToChat: true, + syncSCMToChat: true, enableFastApply: true, chatMode: 'agent', autoApprove: {}, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 74403b9f..2b8f20ba 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -563,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 diff --git a/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts b/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts new file mode 100644 index 00000000..c32fcb08 --- /dev/null +++ b/src/vs/workbench/contrib/void/electron-main/voidSCMMainService.ts @@ -0,0 +1,80 @@ +import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js' +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 => { + 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 => { + 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 => { + 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 { + const useStagedChanges = await hasStagedChanges(path) + const staged = useStagedChanges ? '--staged' : '' + return git(`git diff --stat ${staged}`, path) + } + + async gitSampledDiffs(path: string): Promise { + 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 { + 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(IVoidSCMService, VoidSCMService, InstantiationType.Delayed)