mirror of
https://github.com/voideditor/void
synced 2026-05-23 09:28:23 +00:00
Merge pull request #558 from steaks/commit-prompt
Added Generate Commit Message button for git
This commit is contained in:
commit
7b14f0ca73
11 changed files with 484 additions and 5 deletions
|
|
@ -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);
|
||||
|
||||
|
|
|
|||
|
|
@ -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 }) => {
|
||||
|
|
|
|||
|
|
@ -688,6 +688,7 @@ const ProviderSetting = ({ providerName, settingName, subTextMd }: { providerNam
|
|||
// </div >
|
||||
// }
|
||||
|
||||
|
||||
export const SettingsForProvider = ({ providerName, showProviderTitle, showProviderSuggestions }: { providerName: ProviderName, showProviderTitle: boolean, showProviderSuggestions: boolean }) => {
|
||||
const voidSettingsState = useSettingsState()
|
||||
|
||||
|
|
@ -1341,6 +1342,33 @@ export const Settings = () => {
|
|||
</ErrorBoundary>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* SCM */}
|
||||
<ErrorBoundary>
|
||||
|
||||
<div className='w-full'>
|
||||
<h4 className={`text-base`}>{displayInfoOfFeatureName('SCM')}</h4>
|
||||
<div className='text-sm italic text-void-fg-3 mt-1'>Settings that control the behavior of the commit message generator.</div>
|
||||
|
||||
<div className='my-2'>
|
||||
{/* Sync to Chat Switch */}
|
||||
<div className='flex items-center gap-x-2 my-2'>
|
||||
<VoidSwitch
|
||||
size='xs'
|
||||
value={settingsState.globalSettings.syncSCMToChat}
|
||||
onChange={(newVal) => voidSettingsService.setGlobalSetting('syncSCMToChat', newVal)}
|
||||
/>
|
||||
<span className='text-void-fg-3 text-xs pointer-events-none'>{settingsState.globalSettings.syncSCMToChat ? 'Same as Chat model' : 'Different model'}</span>
|
||||
</div>
|
||||
|
||||
{/* Model Dropdown */}
|
||||
<div className={`my-2 ${settingsState.globalSettings.syncSCMToChat ? 'hidden' : ''}`}>
|
||||
<ModelDropdown featureName={'SCM'} className='text-xs text-void-fg-3 bg-void-bg-1 border border-void-border-1 rounded p-0.5 px-1' />
|
||||
</div>
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
</ErrorBoundary>
|
||||
</div>
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
232
src/vs/workbench/contrib/void/browser/voidSCMService.ts
Normal file
232
src/vs/workbench/contrib/void/browser/voidSCMService.ts
Normal file
|
|
@ -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<void>
|
||||
abort(): void
|
||||
}
|
||||
|
||||
export const IGenerateCommitMessageService = createDecorator<IGenerateCommitMessageService>('voidGenerateCommitMessageService');
|
||||
|
||||
const loadingContextKey = 'voidSCMGenerateCommitMessageLoading'
|
||||
|
||||
class GenerateCommitMessageService extends Disposable implements IGenerateCommitMessageService {
|
||||
readonly _serviceBrand: undefined;
|
||||
private readonly execute = new ThrottledDelayer(300)
|
||||
private llmRequestId: string | null = null
|
||||
private currentRequestId: string | null = null
|
||||
private voidSCM: IVoidSCMService
|
||||
private loadingContextKey: IContextKey<boolean>
|
||||
|
||||
constructor(
|
||||
@ISCMService private readonly scmService: ISCMService,
|
||||
@IMainProcessService mainProcessService: IMainProcessService,
|
||||
@IVoidSettingsService private readonly voidSettingsService: IVoidSettingsService,
|
||||
@IConvertToLLMMessageService private readonly convertToLLMMessageService: IConvertToLLMMessageService,
|
||||
@ILLMMessageService private readonly llmMessageService: ILLMMessageService,
|
||||
@IContextKeyService private readonly contextKeyService: IContextKeyService,
|
||||
@INotificationService private readonly notificationService: INotificationService
|
||||
) {
|
||||
super()
|
||||
this.loadingContextKey = this.contextKeyService.createKey(loadingContextKey, false)
|
||||
this.voidSCM = ProxyChannel.toService<IVoidSCMService>(mainProcessService.getChannel('void-channel-scm'));
|
||||
}
|
||||
|
||||
override dispose() {
|
||||
this.execute.dispose()
|
||||
super.dispose()
|
||||
}
|
||||
|
||||
async generateCommitMessage() {
|
||||
this.loadingContextKey.set(true)
|
||||
this.execute.trigger(async () => {
|
||||
const requestId = generateUuid()
|
||||
this.currentRequestId = requestId
|
||||
|
||||
|
||||
try {
|
||||
const { path, repo } = this.gitRepoInfo()
|
||||
const [stat, sampledDiffs, branch, log] = await Promise.all([
|
||||
this.voidSCM.gitStat(path),
|
||||
this.voidSCM.gitSampledDiffs(path),
|
||||
this.voidSCM.gitBranch(path),
|
||||
this.voidSCM.gitLog(path)
|
||||
])
|
||||
|
||||
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
|
||||
|
||||
const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature['SCM'] ?? null
|
||||
const modelSelectionOptions = modelSelection ? this.voidSettingsService.state.optionsOfModelSelection['SCM'][modelSelection?.providerName]?.[modelSelection.modelName] : undefined
|
||||
const overridesOfModel = this.voidSettingsService.state.overridesOfModel
|
||||
|
||||
const modelOptions: ModelOptions = { modelSelection, modelSelectionOptions, overridesOfModel }
|
||||
|
||||
const prompt = gitCommitMessage_userMessage(stat, sampledDiffs, branch, log)
|
||||
|
||||
const simpleMessages = [{ role: 'user', content: prompt } as const]
|
||||
const { messages, separateSystemMessage } = this.convertToLLMMessageService.prepareLLMSimpleMessages({
|
||||
simpleMessages,
|
||||
systemMessage: gitCommitMessage_systemMessage,
|
||||
modelSelection: modelOptions.modelSelection,
|
||||
featureName: 'SCM',
|
||||
})
|
||||
|
||||
const commitMessage = await this.sendLLMMessage(messages, separateSystemMessage!, modelOptions)
|
||||
|
||||
if (!this.isCurrentRequest(requestId)) { throw new CancellationError() }
|
||||
|
||||
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<string> {
|
||||
return new Promise((resolve, reject) => {
|
||||
|
||||
this.llmRequestId = this.llmMessageService.sendLLMMessage({
|
||||
messagesType: 'chatMessages',
|
||||
messages,
|
||||
separateSystemMessage,
|
||||
chatMode: null,
|
||||
modelSelection: modelOptions.modelSelection,
|
||||
modelSelectionOptions: modelOptions.modelSelectionOptions,
|
||||
overridesOfModel: modelOptions.overridesOfModel,
|
||||
onText: () => { },
|
||||
onFinalMessage: (params: { fullText: string }) => {
|
||||
const match = params.fullText.match(/<output>([\s\S]*?)<\/output>/i)
|
||||
const commitMessage = match ? match[1].trim() : ''
|
||||
resolve(commitMessage)
|
||||
},
|
||||
onError: (error) => {
|
||||
console.error(error)
|
||||
reject(error)
|
||||
},
|
||||
onAbort: () => {
|
||||
reject(new CancellationError())
|
||||
},
|
||||
logging: { loggingName: 'VoidSCM - Commit Message' },
|
||||
})
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
/** Request Helpers */
|
||||
|
||||
private isCurrentRequest(requestId: string) {
|
||||
return requestId === this.currentRequestId
|
||||
}
|
||||
|
||||
|
||||
/** UI Functions */
|
||||
|
||||
private 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<void> {
|
||||
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
|
||||
generateCommitMessageService.generateCommitMessage()
|
||||
}
|
||||
}
|
||||
|
||||
class LoadingGenerateCommitMessageAction extends Action2 {
|
||||
constructor() {
|
||||
super({
|
||||
id: 'void.loadingGenerateCommitMessageAction',
|
||||
title: localize2('voidCommitMessagePromptCancel', 'Void: Cancel Commit Message Generation'),
|
||||
icon: ThemeIcon.fromId('stop-circle'),
|
||||
tooltip: localize2('voidCommitMessagePromptCancelTooltip', 'Void: Cancel Commit Message Generation'),
|
||||
f1: false, //Having a cancel command in the command palette is more confusing than useful.
|
||||
menu: [{
|
||||
id: MenuId.SCMInputBox,
|
||||
when: ContextKeyExpr.and(ContextKeyExpr.equals('scmProvider', 'git'), ContextKeyExpr.equals(loadingContextKey, true)),
|
||||
group: 'inline'
|
||||
}]
|
||||
})
|
||||
}
|
||||
async run(accessor: ServicesAccessor): Promise<void> {
|
||||
const generateCommitMessageService = accessor.get(IGenerateCommitMessageService)
|
||||
generateCommitMessageService.abort()
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IGenerateCommitMessageService, GenerateCommitMessageService, InstantiationType.Delayed)
|
||||
registerAction2(GenerateCommitMessageAction)
|
||||
registerAction2(LoadingGenerateCommitMessageAction)
|
||||
|
|
@ -980,3 +980,77 @@ Store Result: After computing fib(n), the result is stored in memo for future re
|
|||
## END EXAMPLES
|
||||
|
||||
*/
|
||||
|
||||
|
||||
// ======================================================== scm ========================================================================
|
||||
|
||||
export const gitCommitMessage_systemMessage = `
|
||||
You are an expert software engineer AI assistant responsible for writing clear and concise Git commit messages that summarize the **purpose** and **intent** of the change. Try to keep your commit messages to one sentence. If necessary, you can use two sentences.
|
||||
|
||||
You always respond with:
|
||||
- The commit message wrapped in <output> tags
|
||||
- A brief explanation of the reasoning behind the message, wrapped in <reasoning> tags
|
||||
|
||||
Example format:
|
||||
<output>Fix login bug and improve error handling</output>
|
||||
<reasoning>This commit updates the login handler to fix a redirect issue and improves frontend error messages for failed logins.</reasoning>
|
||||
|
||||
Do not include anything else outside of these tags.
|
||||
Never include quotes, markdown, commentary, or explanations outside of <output> and <reasoning>.`.trim()
|
||||
|
||||
|
||||
/**
|
||||
* Create a user message for the LLM to generate a commit message. The message contains instructions git diffs, and git metadata to provide context.
|
||||
*
|
||||
* @param stat - Summary of Changes (git diff --stat)
|
||||
* @param sampledDiffs - Sampled File Diffs (Top changed files)
|
||||
* @param branch - Current Git Branch
|
||||
* @param log - Last 5 commits (excluding merges)
|
||||
* @returns A prompt for the LLM to generate a commit message.
|
||||
*
|
||||
* @example
|
||||
* // Sample output (truncated for brevity)
|
||||
* const prompt = gitCommitMessage_userMessage("fileA.ts | 10 ++--", "diff --git a/fileA.ts...", "main", "abc123|Fix bug|2025-01-01\n...")
|
||||
*
|
||||
* // Result:
|
||||
* Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
|
||||
*
|
||||
* Section 1 - Summary of Changes (git diff --stat):
|
||||
* fileA.ts | 10 ++--
|
||||
*
|
||||
* Section 2 - Sampled File Diffs (Top changed files):
|
||||
* diff --git a/fileA.ts b/fileA.ts
|
||||
* ...
|
||||
*
|
||||
* Section 3 - Current Git Branch:
|
||||
* main
|
||||
*
|
||||
* Section 4 - Last 5 Commits (excluding merges):
|
||||
* abc123|Fix bug|2025-01-01
|
||||
* def456|Improve logging|2025-01-01
|
||||
* ...
|
||||
*/
|
||||
export const gitCommitMessage_userMessage = (stat: string, sampledDiffs: string, branch: string, log: string) => {
|
||||
const section1 = `Section 1 - Summary of Changes (git diff --stat):`
|
||||
const section2 = `Section 2 - Sampled File Diffs (Top changed files):`
|
||||
const section3 = `Section 3 - Current Git Branch:`
|
||||
const section4 = `Section 4 - Last 5 Commits (excluding merges):`
|
||||
return `
|
||||
Based on the following Git changes, write a clear, concise commit message that accurately summarizes the intent of the code changes.
|
||||
|
||||
${section1}
|
||||
|
||||
${stat}
|
||||
|
||||
${section2}
|
||||
|
||||
${sampledDiffs}
|
||||
|
||||
${section3}
|
||||
|
||||
${branch}
|
||||
|
||||
${section4}
|
||||
|
||||
${log}`.trim()
|
||||
}
|
||||
|
|
|
|||
31
src/vs/workbench/contrib/void/common/voidSCMTypes.ts
Normal file
31
src/vs/workbench/contrib/void/common/voidSCMTypes.ts
Normal file
|
|
@ -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<string>
|
||||
/**
|
||||
* Get git diff --stat for the top 10 most significantly changed files according to lines added/removed
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitSampledDiffs(path: string): Promise<string>
|
||||
/**
|
||||
* Get the current git branch
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitBranch(path: string): Promise<string>
|
||||
/**
|
||||
* Get the last 5 commits excluding merges
|
||||
*
|
||||
* @param path Path to the git repository
|
||||
*/
|
||||
gitLog(path: string): Promise<string>
|
||||
}
|
||||
|
||||
export const IVoidSCMService = createDecorator<IVoidSCMService>('voidSCMService')
|
||||
|
|
@ -116,6 +116,7 @@ export const modelFilterOfFeatureName: {
|
|||
'Chat': { filter: o => true, emptyMessage: null, },
|
||||
'Ctrl+K': { filter: o => true, emptyMessage: null, },
|
||||
'Apply': { filter: o => true, emptyMessage: null, },
|
||||
'SCM': { filter: o => true, emptyMessage: null, },
|
||||
}
|
||||
|
||||
|
||||
|
|
@ -213,9 +214,9 @@ const _validatedModelState = (state: Omit<VoidSettingsState, '_modelOptions'>):
|
|||
const defaultState = () => {
|
||||
const d: VoidSettingsState = {
|
||||
settingsOfProvider: deepClone(defaultSettingsOfProvider),
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null },
|
||||
modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null, 'SCM': null },
|
||||
globalSettings: deepClone(defaultGlobalSettings),
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {} },
|
||||
optionsOfModelSelection: { 'Chat': {}, 'Ctrl+K': {}, 'Autocomplete': {}, 'Apply': {}, 'SCM': {} },
|
||||
overridesOfModel: deepClone(defaultOverridesOfModel),
|
||||
_modelOptions: [], // computed later
|
||||
mcpUserStateOfName: {},
|
||||
|
|
@ -262,6 +263,7 @@ class VoidSettingsService extends Disposable implements IVoidSettingsService {
|
|||
await this._storeState()
|
||||
this._onDidChangeState.fire()
|
||||
this._onUpdate_syncApplyToChat()
|
||||
this._onUpdate_syncSCMToChat()
|
||||
}
|
||||
async resetState() {
|
||||
await this.dangerousSetState(defaultState())
|
||||
|
|
@ -280,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()
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -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: {},
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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<string> => {
|
||||
const { stdout, stderr } = await exec(`${command}`, { cwd: path })
|
||||
if (stderr) {
|
||||
throw new Error(stderr)
|
||||
}
|
||||
return stdout.trim()
|
||||
}
|
||||
|
||||
const getNumStat = async (path: string, useStagedChanges: boolean): Promise<NumStat[]> => {
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
const output = await git(`git diff --numstat ${staged}`, path)
|
||||
return output
|
||||
.split('\n')
|
||||
.map((line) => {
|
||||
const [added, removed, file] = line.split('\t')
|
||||
return {
|
||||
file,
|
||||
added: parseInt(added, 10) || 0,
|
||||
removed: parseInt(removed, 10) || 0,
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
const getSampledDiff = async (file: string, path: string, useStagedChanges: boolean): Promise<string> => {
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
const diff = await git(`git diff --unified=0 --no-color ${staged} -- "${file}"`, path)
|
||||
return diff.slice(0, MAX_DIFF_LENGTH)
|
||||
}
|
||||
|
||||
const hasStagedChanges = async (path: string): Promise<boolean> => {
|
||||
const output = await git('git diff --staged --name-only', path)
|
||||
return output.length > 0
|
||||
}
|
||||
|
||||
export class VoidSCMService implements IVoidSCMService {
|
||||
readonly _serviceBrand: undefined
|
||||
|
||||
async gitStat(path: string): Promise<string> {
|
||||
const useStagedChanges = await hasStagedChanges(path)
|
||||
const staged = useStagedChanges ? '--staged' : ''
|
||||
return git(`git diff --stat ${staged}`, path)
|
||||
}
|
||||
|
||||
async gitSampledDiffs(path: string): Promise<string> {
|
||||
const useStagedChanges = await hasStagedChanges(path)
|
||||
const numStatList = await getNumStat(path, useStagedChanges)
|
||||
const topFiles = numStatList
|
||||
.sort((a, b) => (b.added + b.removed) - (a.added + a.removed))
|
||||
.slice(0, MAX_DIFF_FILES)
|
||||
const diffs = await Promise.all(topFiles.map(async ({ file }) => ({ file, diff: await getSampledDiff(file, path, useStagedChanges) })))
|
||||
return diffs.map(({ file, diff }) => `==== ${file} ====\n${diff}`).join('\n\n')
|
||||
}
|
||||
|
||||
gitBranch(path: string): Promise<string> {
|
||||
return git('git branch --show-current', path)
|
||||
}
|
||||
|
||||
gitLog(path: string): Promise<string> {
|
||||
return git('git log --pretty=format:"%h|%s|%ad" --date=short --no-merges -n 5', path)
|
||||
}
|
||||
}
|
||||
|
||||
registerSingleton(IVoidSCMService, VoidSCMService, InstantiationType.Delayed)
|
||||
Loading…
Reference in a new issue