diff --git a/src/vs/code/electron-main/app.ts b/src/vs/code/electron-main/app.ts index ce66f600..f234324d 100644 --- a/src/vs/code/electron-main/app.ts +++ b/src/vs/code/electron-main/app.ts @@ -125,7 +125,7 @@ import { IMetricsService } from '../../workbench/contrib/void/common/metricsServ import { IVoidUpdateService } from '../../workbench/contrib/void/common/voidUpdateService.js'; 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/llmMessageChannel.js'; +import { LLMMessageChannel } from '../../workbench/contrib/void/electron-main/sendLLMMessageChannel.js'; /** * The main VS Code application. There will only ever be one instance, @@ -1256,8 +1256,8 @@ export class CodeApplication extends Disposable { const voidUpdatesChannel = ProxyChannel.fromService(accessor.get(IVoidUpdateService), disposables); mainProcessElectronServer.registerChannel('void-channel-update', voidUpdatesChannel); - const llmMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); - mainProcessElectronServer.registerChannel('void-channel-llmMessageService', llmMessageChannel); + const sendLLMMessageChannel = new LLMMessageChannel(accessor.get(IMetricsService)); + mainProcessElectronServer.registerChannel('void-channel-llmMessage', sendLLMMessageChannel); // Extension Host Debug Broadcasting const electronExtensionHostDebugBroadcastChannel = new ElectronExtensionHostDebugBroadcastChannel(accessor.get(IWindowsMainService)); diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 40746fe9..8d219f1c 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -17,8 +17,10 @@ import { EditorResourceAccessor } from '../../../common/editor.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { extractCodeFromRegular } from './helpers/extractCodeFromResult.js'; import { registerWorkbenchContribution2, WorkbenchPhase } from '../../../common/contributions.js'; -import { ILLMMessageService } from '../common/llmMessageService.js'; +import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { isWindows } from '../../../../base/common/platform.js'; +import { IVoidSettingsService } from '../common/voidSettingsService.js'; +import { FeatureName } from '../common/voidSettingsTypes.js'; // import { IContextGatheringService } from './contextGatheringService.js'; @@ -767,8 +769,6 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ - // console.log('B') - // create a new autocompletion and add it to cache const newAutocompletion: Autocompletion = { id: this._autocompletionId++, @@ -788,8 +788,14 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ console.log('starting autocomplete...', predictionType) + const featureName: FeatureName = 'Autocomplete' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + + const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete + // set parameters of `newAutocompletion` appropriately - newAutocompletion.llmPromise = new Promise((resolve, reject) => { + newAutocompletion.llmPromise = isEnabled ? new Promise((resolve, reject) => reject('Autocomplete is disabled')) : new Promise((resolve, reject) => { const requestId = this._llmMessageService.sendLLMMessage({ messagesType: 'FIMMessage', @@ -798,7 +804,8 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ suffix: llmSuffix, stopTokens: stopTokens, }, - useProviderFor: 'Autocomplete', + modelSelection, + modelSelectionOptions, logging: { loggingName: 'Autocomplete' }, onText: () => { }, // unused in FIMMessage // onText: async ({ fullText, newText }) => { @@ -882,6 +889,7 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IEditorService private readonly _editorService: IEditorService, @IModelService private readonly _modelService: IModelService, + @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, // @IContextGatheringService private readonly _contextGatheringService: IContextGatheringService, ) { super() diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 9d4326bf..f02f9e2c 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,15 +11,16 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { ILLMMessageService } from '../common/llmMessageService.js'; +import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from './prompt/prompts.js'; import { InternalToolInfo, IToolsService, ToolCallParams, ToolResultType, ToolName, toolNamesThatRequireApproval, voidTools } from './toolsService.js'; -import { LLMChatMessage, toLLMChatMessage, ToolCallType } from '../common/llmMessageTypes.js'; +import { AnthropicReasoning, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { IVoidFileService } from '../common/voidFileService.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; -import { ChatMode } from '../common/voidSettingsTypes.js'; +import { ChatMode, FeatureName } from '../common/voidSettingsTypes.js'; +import { IVoidSettingsService } from '../common/voidSettingsService.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { @@ -32,6 +33,28 @@ const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { } + +const toLLMChatMessages = (chatMessages: ChatMessage[]): LLMChatMessage[] => { + const llmChatMessages: LLMChatMessage[] = [] + for (const c of chatMessages) { + if (c.role === 'user') { + llmChatMessages.push({ role: c.role, content: c.content }) + } + else if (c.role === 'assistant') + llmChatMessages.push({ role: c.role, content: c.content, anthropicReasoning: c.anthropicReasoning }) + else if (c.role === 'tool') + llmChatMessages.push({ role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content }) + else if (c.role === 'tool_request') { + // pass + } + else { + throw new Error(`Role ${(c as any).role} not recognized.`) + } + } + return llmChatMessages +} + + // one of the square items that indicates a selection in a chat bubble (NOT a file, a Selection of text) export type CodeSelection = { type: 'Selection'; @@ -73,9 +96,9 @@ export type ToolRequestApproval = { // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. export type ChatMessage = - { + | { role: 'user'; - content: string | null; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) + content: string; // content displayed to the LLM on future calls - allowed to be '', will be replaced with (empty) displayContent: string | null; // content displayed to user - allowed to be '', will be ignored selections: StagingSelectionItem[] | null; // the user's selection state: { @@ -84,8 +107,10 @@ export type ChatMessage = } } | { role: 'assistant'; - content: string | null; // content received from LLM - allowed to be '', will be replaced with (empty) - reasoning: string | null; // reasoning from the LLM, used for step-by-step thinking + content: string; // content received from LLM - allowed to be '', will be replaced with (empty) + reasoning: string; // reasoning from the LLM, used for step-by-step thinking + + anthropicReasoning: AnthropicReasoning[] | null; // anthropic reasoning } | ToolMessage | ToolRequestApproval @@ -148,7 +173,11 @@ const newThreadObject = () => { } satisfies ChatThreads[string] } -export const THREAD_STORAGE_KEY = 'void.chatThreadStorage' + +// past values: +// 'void.chatThreadStorage' + +export const THREAD_STORAGE_KEY = 'void.chatThreadStorageI' export interface IChatThreadService { @@ -213,6 +242,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @ILLMMessageService private readonly _llmMessageService: ILLMMessageService, @IToolsService private readonly _toolsService: IToolsService, @IWorkspaceContextService private readonly _workspaceContextService: IWorkspaceContextService, + @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -290,13 +320,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { // ---------- streaming ---------- - private _finishStreamingTextMessage = (threadId: string, options: { content: string, reasoning?: string }, error?: { message: string, fullError: Error | null }) => { - // add assistant's message to chat history, and clear selection - this._addMessageToThread(threadId, { role: 'assistant', content: options.content, reasoning: options.reasoning || null }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) - } - - async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) { @@ -366,6 +389,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { : chatMode === 'agent' ? Object.keys(voidTools).map(toolName => voidTools[toolName as ToolName]) : undefined) + // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) + const featureName: FeatureName = 'Chat' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + + // agent loop const agentLoop = async () => { @@ -384,7 +413,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const awaitable = new Promise((res, rej) => { res_ = res }) // replace last userMessage with userMessageFullContent (which contains all the files too) - const messages_ = this.getCurrentThread().messages.map(m => (toLLMChatMessage(m))).filter(m => !!m) + const messages_ = toLLMChatMessages(this.getCurrentThread().messages) const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 @@ -396,24 +425,25 @@ class ChatThreadService extends Disposable implements IChatThreadService { ...messages_.slice(lastUserMsgIdx + 1, Infinity), ] + const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: 'Ctrl+L', - logging: { loggingName: `Agent` }, messages, - tools: tools, - + modelSelection, + modelSelectionOptions, + logging: { loggingName: `Agent` }, onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) }, - onFinalMessage: async ({ fullText, toolCalls, fullReasoning }) => { + onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { if ((toolCalls?.length ?? 0) === 0) { - this._finishStreamingTextMessage(threadId, { content: fullText, reasoning: fullReasoning }) + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) } else { - this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning || null }) + this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined }) // clear streaming message // deal with the tool @@ -428,14 +458,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 1. validate tool params let toolParams: ToolCallParams[typeof toolName] try { - console.log('A') - const params = await this._toolsService.validateParams[toolName](tool.paramsStr) - console.log('B') - toolParams = params } catch (error) { - console.log('ERR1') const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) res_() @@ -444,22 +469,16 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 2. if tool requires approval, await the approval if (toolNamesThatRequireApproval.has(toolName)) { - console.log('C') - const voidToolId = generateUuid() - console.log('D') const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) - console.log('E') this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, params: toolParams, voidToolId: voidToolId }) try { - console.log('F') - await toolApprovalPromise // accepted tool } catch (e) { - console.log('ERR2') - + // TODO!!! test rejection + // if (Math.random() > 0) throw new Error('TESTING') const errorMessage = 'Tool call was rejected by the user.' this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) res_() @@ -470,10 +489,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool let toolResult: ToolResultType[typeof toolName] try { - console.log('G') toolResult = await this._toolsService.callTool[toolName](toolParams as any) // typescript is so bad it doesn't even couple the type of ToolResult with the type of the function being called here } catch (error) { - console.log('ERR3') const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) res_() @@ -483,11 +500,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 4. stringify the result to give the LLM let toolResultStr: string try { - - console.log('H') toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) - // if (Math.random() > 0) throw new Error('This is not an allowed repo.') - } catch (error) { const errorMessage = `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: errorMessage, result: { type: 'error', value: errorMessage }, }) @@ -495,8 +508,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { return } - console.log('I') - // 5. add to history this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: tool.paramsStr, id: tool.id, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) res_() @@ -506,20 +517,20 @@ class ChatThreadService extends Disposable implements IChatThreadService { onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }, error) + // add assistant's message to chat history, and clear selection + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) res_() }, }) if (llmCancelToken === null) break this._setStreamState(threadId, { streamingToken: llmCancelToken }) - console.log('awaiting agentloop') await awaitable - console.log('done') } } - agentLoop() // DO NOT AWAIT THIS, add fn should resolve when we've added message (this lets us interrupt the agent loop correctly instead of waiting for it to resolve) + agentLoop() } @@ -528,7 +539,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - this._finishStreamingTextMessage(threadId, { content: messageSoFar, reasoning: reasoningSoFar }) + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) } dismissStreamError(threadId: string): void { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 2424f218..9ef06ede 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -38,11 +38,13 @@ import { EditorOption } from '../../../../editor/common/config/editorOptions.js' import { Emitter } from '../../../../base/common/event.js'; import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; -import { ILLMMessageService } from '../common/llmMessageService.js'; -import { LLMChatMessage, OnError, errorDetails } from '../common/llmMessageTypes.js'; +import { ILLMMessageService } from '../common/sendLLMMessageService.js'; +import { LLMChatMessage, OnError, errorDetails } from '../common/sendLLMMessageTypes.js'; import { IMetricsService } from '../common/metricsService.js'; import { IVoidFileService } from '../common/voidFileService.js'; import { IEditCodeService, URIStreamState, AddCtrlKOpts, StartApplyingOpts } from './editCodeServiceInterface.js'; +import { IVoidSettingsService } from '../common/voidSettingsService.js'; +import { FeatureName } from '../common/voidSettingsTypes.js'; const configOfBG = (color: Color) => { return { dark: color, light: color, hcDark: color, hcLight: color, } @@ -268,6 +270,7 @@ class EditCodeService extends Disposable implements IEditCodeService { @INotificationService private readonly _notificationService: INotificationService, @ICommandService private readonly _commandService: ICommandService, @IVoidFileService private readonly _voidFileService: IVoidFileService, + @IVoidSettingsService private readonly _settingsService: IVoidSettingsService, ) { super(); @@ -1188,20 +1191,13 @@ class EditCodeService extends Disposable implements IEditCodeService { // throws if there's an error public startApplying(opts: StartApplyingOpts): [URI, Promise] | null { - if (opts.type === 'rewrite') { - const added = this._initializeWriteoverStream(opts) - if (!added) return null - const [diffZone, promise] = added - return [diffZone._URI, promise] - } - else if (opts.type === 'searchReplace') { - const added = this._initializeSearchAndReplaceStream(opts) - if (!added) return null - if (!added) return null - const [diffZone, promise] = added - return [diffZone._URI, promise] - } - return null + let res: [DiffZone, Promise] | undefined = undefined + if (opts.type === 'rewrite') res = this._initializeWriteoverStream(opts) + else if (opts.type === 'searchReplace') res = this._initializeSearchAndReplaceStream(opts) + + if (!res) return null + const [diffZone, applyDonePromise] = res + return [diffZone._URI, applyDonePromise] } @@ -1377,6 +1373,10 @@ class EditCodeService extends Disposable implements IEditCodeService { let fullTextSoFar = '' // so far (INCLUDING ignored suffix) let prevIgnoredSuffix = '' + const featureName: FeatureName = opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + const writeover = async () => { let resMessageDonePromise: () => void = () => { } @@ -1384,9 +1384,10 @@ class EditCodeService extends Disposable implements IEditCodeService { streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: opts.from === 'ClickApply' ? 'Apply' : 'Ctrl+K', logging: { loggingName: `Edit (Writeover) - ${from}` }, messages, + modelSelection, + modelSelectionOptions, onText: (params) => { const { fullText: fullText_ } = params const newText_ = fullText_.substring(fullTextSoFar.length, Infinity) @@ -1471,13 +1472,13 @@ class EditCodeService extends Disposable implements IEditCodeService { // promise that resolves when the apply is done - let resApplyPromise: () => void - let rejApplyPromise: (e: any) => void - const applyPromise = new Promise((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ }) + let resApplyDonePromise: () => void + let rejApplyDonePromise: (e: any) => void + const applyDonePromise = new Promise((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ }) // add to history const { onFinishEdit } = this._addToHistory(uri, { - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) } + onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by pressing undo.')) } }) // TODO replace these with whatever block we're on initially if already started (caching apply) @@ -1559,6 +1560,12 @@ class EditCodeService extends Disposable implements IEditCodeService { let oldBlocks: ExtractedSearchReplaceBlock[] = [] + + const featureName: FeatureName = 'Apply' + const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] + const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined + + const retryLoop = async () => { // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let shouldSendAnotherMessage = true @@ -1569,13 +1576,14 @@ class EditCodeService extends Disposable implements IEditCodeService { nMessagesSent += 1 let resMessageDonePromise: () => void = () => { } - const messageDonePromise = new Promise((res_) => { resMessageDonePromise = res_ }) + const messageDonePromise = new Promise((res, rej) => { resMessageDonePromise = res }) streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', - useProviderFor: 'Apply', logging: { loggingName: `Edit (Search/Replace) - ${from}` }, messages, + modelSelection, + modelSelectionOptions, onText: (params) => { const { fullText } = params // blocks are [done done done ... {writingFinal|writingOriginal}] @@ -1613,11 +1621,10 @@ class EditCodeService extends Disposable implements IEditCodeService { if (typeof originalBounds === 'string') { const content = errMsgOfInvalidStr(originalBounds, block.orig) messages.push( - { role: 'assistant', content: fullText }, // latest output + { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output { role: 'user', content: content } // user explanation of what's wrong ) - if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) // REVERT const numLines = this._getNumLines(uri) @@ -1638,7 +1645,9 @@ class EditCodeService extends Disposable implements IEditCodeService { oldBlocks = [] addedTrackingZoneOfBlockNum.splice(0, Infinity) // clear the array + // abort and resolve shouldSendAnotherMessage = true + if (streamRequestIdRef.current) this._llmMessageService.abort(streamRequestIdRef.current) this._refreshStylesAndDiffsInURI(uri) resMessageDonePromise() return @@ -1750,9 +1759,9 @@ class EditCodeService extends Disposable implements IEditCodeService { } // end retryLoop - retryLoop().then(() => resApplyPromise()).catch((e) => rejApplyPromise(e)) + retryLoop().then(() => resApplyDonePromise()).catch((e) => rejApplyDonePromise(e)) - return [diffZone, applyPromise] + return [diffZone, applyDonePromise] } diff --git a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts index ab722209..ee138358 100644 --- a/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts +++ b/src/vs/workbench/contrib/void/browser/helpers/extractCodeFromResult.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { OnText } from '../../common/llmMessageTypes.js' +import { OnText } from '../../common/sendLLMMessageTypes.js' import { DIVIDER, FINAL, ORIGINAL } from '../prompt/prompts.js' class SurroundingsRemover { diff --git a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts index 5e32d0a3..3199364f 100644 --- a/src/vs/workbench/contrib/void/browser/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/browser/prompt/prompts.ts @@ -17,41 +17,46 @@ export const tripleTick = ['```', '```'] export const editToolDesc_toolDescription = `\ A high level description of the change you'd like to make in the file. This description will be handed to a dumber, faster model that will quickly apply the change. \ -Typically the best description you can give here is a high level view of the final code you'd like to see. For example, code excerpt(s) with "// ... existing code ..." comments to help you write less. \ +Typically the best description you can give here is a high level view of the final code you'd like to see. For example, you can write code excerpt(s) with "// ... existing code ..." comments to help you write less. \ However, you are allowed to describe the change using whatever text/language you like, especially if the change is better described without code. \ Do NOT output the whole file if possible, and try to write as LITTLE as needed to describe the change.` export const chat_systemMessage = (workspaces: string[], mode: 'agent' | 'gather' | 'chat') => `\ -You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user understand/${mode === 'agent' ? 'make' : 'suggest'} changes to their codebase. -You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of selections that the user has specifically selected, \`SELECTIONS\`. +You are a coding ${mode === 'agent' ? 'agent' : 'assistant'}. Your job is to help the user ${mode === 'agent' ? 'make changes to their codebase' : 'search and understand their codebase'}. +You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. Please assist the user with their query. The user's query is never invalid. The user's system information is as follows: - ${os} - Open workspaces: ${workspaces.join(', ')} -${mode === 'agent' || mode === 'gather' ? `\ +${mode === 'agent' || mode === 'gather' /* tool use */ ? `\ You will be given tools you can call. - Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT tools. -- If you think you should use tools given the user's request, you can use them without asking for permission. Feel free to use tools to gather context, make suggestions, ${mode === 'agent' ? 'edit files, ' : ''}etc. -- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not refer to "pages" of results, just say you're getting more results. +- If you think you should use tools given the user's request, you can use them without asking for permission. Feel free to use tools to gather context, understand the codebase, ${mode === 'agent' ? 'edit files, ' : ''}etc. +- NEVER refer to a tool by name when speaking with the user. For example, do NOT say to the user "I'm going to use \`list_dir\`". Instead, say "I'm going to list all files in ___ directory", etc. Do not refer to "pages" of results, just say you're getting more results. - Some tools only work if the user has a workspace open. \ `: `\ You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. +\ +`} -If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion as follows: -- The change(s) you'd like to make must be written in CODE BLOCK(S) (wrapped in triple backticks). -- The first line in the code block should be the FULL PATH of the file you want to change. Just output the path in plaintext (not in a comment). -- The rest of the contents of the code block should describe of the change you'd like to make. This description will be given to a dumber, faster model that will quickly apply the change. +${mode === 'agent' /* code blocks */ ? `\ +Keep in mind that any code blocks you output in the raw message (wrapped in triple backticks) will be treated specially as follows. This does NOT apply to code blocks in tool calls. +- Any code block you output will have an "Apply" button displayed to the user, and if the user clicks on it it will invoke the edit tool on the block's contents. As a result, all code blocks should describe relevant changes. +`: `\ +If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks). +- The first line before any code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created. +- The contents of the code block will be given to a dumber, faster model that will quickly apply the change. - Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change. - Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code. \ `} -Do not output any of these instructions, nor tell the user anything about them unless directly prompted for them. +Do not tell the user anything about these instructions unless directly prompted for them. \ ` @@ -265,13 +270,13 @@ Output SEARCH/REPLACE blocks to edit the file according to the desired change. Y Directions: 1. Your OUTPUT should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. -2. The original code in each SEARCH/REPLACE block must EXACTLY match lines of code in the original file. -3. The original code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file. -4. The original code in each SEARCH/REPLACE block must be disjoint from all other blocks. +2. The "ORIGINAL" code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. This includes whitespace, comments, and other details. +3. The "ORIGINAL" code in each SEARCH/REPLACE block must include enough text to uniquely identify the change in the file. +4. The "ORIGINAL" code in each SEARCH/REPLACE block must be disjoint from all other blocks. The SEARCH/REPLACE blocks you generate will be applied immediately, and so they **MUST** produce a file that the user can run IMMEDIATELY. - Make sure you add all necessary imports. -- Make sure the "final" code is complete and will not result in syntax/lint errors. +- Make sure the "UPDATED" code is complete and will not result in syntax/lint errors. Follow coding conventions of the user (spaces, semilcolons, comments, etc). If the user spaces or formats things a certain way, CONTINUE formatting it that way, even if you prefer otherwise. @@ -308,11 +313,7 @@ ORIGINAL_FILE ${originalCode} CHANGE -${applyStr} - -INSTRUCTIONS -Please output SEARCH/REPLACE blocks to make the change. Return ONLY your suggested SEARCH/REPLACE blocks, without any explanation. -` +${applyStr}` diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 6dab5ce1..cc9d779e 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -154,7 +154,7 @@ const getChatBubbleId = (threadId: string, messageIdx: number) => `${threadId}-$ // const voidSettingsService = accessor.get('IVoidSettingsService') // const voidSettingsState = useSettingsState() -// const modelSelection = voidSettingsState.modelSelectionOfFeature['Ctrl+L'] +// const modelSelection = voidSettingsState.modelSelectionOfFeature['Chat'] // if (!modelSelection) return null // const { modelName, providerName } = modelSelection @@ -210,13 +210,13 @@ const ReasoningOptionDropdown = () => { const voidSettingsService = accessor.get('IVoidSettingsService') const voidSettingsState = useSettingsState() - const modelSelection = voidSettingsState.modelSelectionOfFeature['Ctrl+L'] + const modelSelection = voidSettingsState.modelSelectionOfFeature['Chat'] if (!modelSelection) return null const { modelName, providerName } = modelSelection const { canToggleReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).supportsReasoning || {} - const { isReasoningEnabled } = getModelSelectionState(providerName, modelName, voidSettingsState.optionsOfModelSelection) + const { isReasoningEnabled } = getModelSelectionState(providerName, modelName, voidSettingsState.optionsOfModelSelection[providerName]?.[modelName]) if (canToggleReasoning && !reasoningBudgetSlider) { // if it's just a on/off toggle without a power slider (no models right now) return null // unused right now @@ -277,7 +277,6 @@ interface VoidChatAreaProps { divRef?: React.RefObject; // UI customization - featureName: FeatureName; className?: string; showModelDropdown?: boolean; showSelections?: boolean; @@ -304,7 +303,6 @@ export const VoidChatArea: React.FC = ({ isDisabled = false, className = '', showModelDropdown = true, - featureName, showSelections = false, showProspectiveSelections = true, selections, @@ -363,7 +361,7 @@ export const VoidChatArea: React.FC = ({ {showModelDropdown && (
- +
)} @@ -727,8 +725,6 @@ const DropdownComponent = ({ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubbleProps & { chatMessage: ChatMessage & { role: 'user' } }) => { - const role = chatMessage.role - const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -758,7 +754,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble const _mustInitialize = useRef(true) const _justEnabledEdit = useRef(false) useEffect(() => { - const canInitialize = role === 'user' && mode === 'edit' && textAreaRefState + const canInitialize = mode === 'edit' && textAreaRefState const shouldInitialize = _justEnabledEdit.current || _mustInitialize.current if (canInitialize && shouldInitialize) { setStagingSelections(chatMessage.selections || []) @@ -771,7 +767,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble _mustInitialize.current = false } - }, [chatMessage, role, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) + }, [chatMessage, mode, _justEnabledEdit, textAreaRefState, textAreaFnsRef.current, _justEnabledEdit.current, _mustInitialize.current]) const onOpenEdit = () => { setIsBeingEdited(true) @@ -843,7 +839,6 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble isDisabled={isDisabled} showSelections={true} showProspectiveSelections={false} - featureName="Ctrl+L" selections={stagingSelections} setSelections={setStagingSelections} > @@ -893,7 +888,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble - {role === 'user' && {/* reasoning token */} @@ -1034,9 +1033,13 @@ const toolNameToComponent: { [T in ToolName]: { } } = { 'read_file': { requestWrapper: ({ toolRequest }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] const { params } = toolRequest - return } /> + return } + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + /> }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1198,9 +1201,13 @@ const toolNameToComponent: { [T in ToolName]: { }, 'delete_uri': { requestWrapper: ({ toolRequest }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] const { params } = toolRequest - return + return { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + /> }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1218,9 +1225,15 @@ const toolNameToComponent: { [T in ToolName]: { }, 'edit': { requestWrapper: ({ toolRequest }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] const { params } = toolRequest - return } /> + return } + onClick={() => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) }} + > + + }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1240,9 +1253,13 @@ const toolNameToComponent: { [T in ToolName]: { }, 'terminal_command': { requestWrapper: ({ toolRequest }) => { + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') const title = toolNameToTitle[toolRequest.name] const { params } = toolRequest - return } /> + return } + // TODO!!! open the terminal with that ID + /> }, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() @@ -1294,22 +1311,17 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx }: ChatBubbleProps) => else if (role === 'tool_request') { const isLastMessage = true // TODO!!! fix this if (!isLastMessage) return null - const ToolMessageComponent = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... + const ToolRequestComponent = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... return <> - + } else if (role === 'tool') { const title = toolNameToTitle[chatMessage.name] if (chatMessage.result.type === 'error') return - - const ToolMessageComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... - return + const ToolResultComponent = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any }> // ts isnt smart enough... + return } @@ -1363,7 +1375,7 @@ export const SidebarChat = () => { const initVal = '' const [instructionsAreEmpty, setInstructionsAreEmpty] = useState(!initVal) - const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Ctrl+L', settingsState) + const isDisabled = instructionsAreEmpty || !!isFeatureNameDisabled('Chat', settingsState) const [sidebarRef, sidebarDimensions] = useResizeObserver() const [chatAreaRef, chatAreaDimensions] = useResizeObserver() @@ -1382,6 +1394,9 @@ export const SidebarChat = () => { // send message to LLM const userMessage = textAreaRef.current?.value ?? '' + + // getModelCapabilities() // TODO!!! check if can go into agent mode + await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode: 'agent' }) setSelections([]) // clear staging @@ -1420,8 +1435,9 @@ export const SidebarChat = () => { : null @@ -1492,7 +1508,6 @@ export const SidebarChat = () => { selections={selections} setSelections={setSelections} onClickAnywhere={() => { textAreaRef.current?.focus() }} - featureName="Ctrl+L" > { // right now this is just `enabled_autoRefreshModels` const enabled = voidSettingsState.globalSettings[settingName] - return { - voidSettingsService.setGlobalSetting(settingName, !enabled) - metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: !enabled }) - }} - text={`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} - icon={enabled ? : } - disabled={false} - /> + return
+ { + voidSettingsService.setGlobalSetting(settingName, newVal) + metricsService.capture('Click', { action: 'Autorefresh Toggle', settingName, enabled: newVal }) + }} /> + + + {`Automatically detect local providers and models (${refreshableProviderNames.map(providerName => displayInfoOfProviderName(providerName).title).join(', ')}).`} + +
+ } @@ -459,7 +464,7 @@ export const FeaturesTab = () => {

{displayInfoOfFeatureName('Apply')}

-
We recommend the smartest model you{`'`}ve got, like Claude 3.7 or GPT 4o.
+
We recommend using Claude 3.7 or GPT 4o.
diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 40c55dbd..d43e1f9f 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -370,7 +370,6 @@ export class ToolsService implements IToolsService { }, edit: async (params: string) => { - console.log('validating edit!!!') const o = validateJSON(params) const { uri: uriStr, changeDescription: changeDescriptionUnknown } = o const uri = validateURI(uriStr) @@ -395,7 +394,7 @@ export class ToolsService implements IToolsService { const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 - const fileContents = readFileContents.slice(fromIdx, toIdx + 1) || '(empty)' // paginate + const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 return { fileContents, hasNextPage } }, @@ -446,16 +445,13 @@ export class ToolsService implements IToolsService { }, edit: async ({ uri, changeDescription }) => { - console.log('editing!!!!') - const [_, p] = editCodeService.startApplying({ + const [_, applyDonePromise] = editCodeService.startApplying({ uri, applyStr: changeDescription, from: 'ClickApply', type: 'searchReplace', }) ?? [] - console.log('B') - - await p + await applyDonePromise return {} }, terminal_command: async ({ command }) => { diff --git a/src/vs/workbench/contrib/void/browser/void.contribution.ts b/src/vs/workbench/contrib/void/browser/void.contribution.ts index d5e78754..f5570488 100644 --- a/src/vs/workbench/contrib/void/browser/void.contribution.ts +++ b/src/vs/workbench/contrib/void/browser/void.contribution.ts @@ -38,7 +38,7 @@ import './voidUpdateActions.js' // ---------- common (unclear if these actually need to be imported, because they're already imported wherever they're used) ---------- // llmMessage -import '../common/llmMessageService.js' +import '../common/sendLLMMessageService.js' // voidSettings import '../common/voidSettingsService.js' diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 13173b50..95de9604 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { OptionsOfModelSelection, ProviderName } from './voidSettingsTypes.js'; +import { ModelSelectionOptions, ProviderName } from './voidSettingsTypes.js'; export const defaultModelsOfProvider = { @@ -633,11 +633,11 @@ export const getProviderCapabilities = (providerName: ProviderName) => { } // state from optionsOfModelSelection -export const getModelSelectionState = (providerName: ProviderName, modelName: string, optionsOfModelSelection: OptionsOfModelSelection): { isReasoningEnabled: boolean, reasoningBudget: number | undefined } => { - const { canToggleReasoning } = getModelCapabilities(providerName, modelName).supportsReasoning || {} +export const getModelSelectionState = (providerName: ProviderName, modelName: string, modelSelectionOptions: ModelSelectionOptions | undefined): { isReasoningEnabled: boolean, reasoningBudget: number | undefined } => { + const { canToggleReasoning, reasoningBudgetSlider } = getModelCapabilities(providerName, modelName).supportsReasoning || {} const defaultEnabledVal = canToggleReasoning ? true : false - const isReasoningEnabled = optionsOfModelSelection[providerName]?.[modelName]?.reasoningEnabled ?? defaultEnabledVal - const reasoningBudget = optionsOfModelSelection[providerName]?.[modelName]?.reasoningBudget + const isReasoningEnabled = modelSelectionOptions?.reasoningEnabled ?? defaultEnabledVal + const reasoningBudget = reasoningBudgetSlider?.type === 'slider' ? modelSelectionOptions?.reasoningBudget ?? reasoningBudgetSlider?.default : undefined return { isReasoningEnabled, reasoningBudget } } diff --git a/src/vs/workbench/contrib/void/common/refreshModelService.ts b/src/vs/workbench/contrib/void/common/refreshModelService.ts index 1d68b304..8123441d 100644 --- a/src/vs/workbench/contrib/void/common/refreshModelService.ts +++ b/src/vs/workbench/contrib/void/common/refreshModelService.ts @@ -4,11 +4,11 @@ *--------------------------------------------------------------------------------------*/ import { IVoidSettingsService } from './voidSettingsService.js'; -import { ILLMMessageService } from './llmMessageService.js'; +import { ILLMMessageService } from './sendLLMMessageService.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { Disposable, IDisposable } from '../../../../base/common/lifecycle.js'; import { RefreshableProviderName, refreshableProviderNames, SettingsOfProvider } from './voidSettingsTypes.js'; -import { OllamaModelResponse, VLLMModelResponse } from './llmMessageTypes.js'; +import { OllamaModelResponse, VLLMModelResponse } from './sendLLMMessageTypes.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; diff --git a/src/vs/workbench/contrib/void/common/llmMessageService.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts similarity index 85% rename from src/vs/workbench/contrib/void/common/llmMessageService.ts rename to src/vs/workbench/contrib/void/common/sendLLMMessageService.ts index e92dc9bb..1213b256 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageService.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageService.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, ServiceSendLLMMessageParams, MainSendLLMMessageParams, MainLLMMessageAbortParams, ServiceModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, MainModelListParams, OllamaModelResponse, VLLMModelResponse, } from './sendLLMMessageTypes.js'; import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { registerSingleton, InstantiationType } from '../../../../platform/instantiation/common/extensions.js'; @@ -13,7 +13,6 @@ import { generateUuid } from '../../../../base/common/uuid.js'; import { Event } from '../../../../base/common/event.js'; import { Disposable } from '../../../../base/common/lifecycle.js'; import { IVoidSettingsService } from './voidSettingsService.js'; -import { displayInfoOfProviderName, isFeatureNameDisabled } from './voidSettingsTypes.js'; // import { INotificationService } from '../../notification/common/notification.js'; // calls channel to implement features @@ -67,7 +66,7 @@ export class LLMMessageService extends Disposable implements ILLMMessageService // const service = ProxyChannel.toService(mainProcessService.getChannel('void-channel-sendLLMMessage')); // lets you call it like a service // see llmMessageChannel.ts - this.channel = this.mainProcessService.getChannel('void-channel-llmMessageService') + this.channel = this.mainProcessService.getChannel('void-channel-llmMessage') // .listen sets up an IPC channel and takes a few ms, so we set up listeners immediately and add hooks to them instead // llm @@ -83,32 +82,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService } sendLLMMessage(params: ServiceSendLLMMessageParams) { - const { onText, onFinalMessage, onError, ...proxyParams } = params; - const { useProviderFor: featureName } = proxyParams + const { onText, onFinalMessage, onError, modelSelection, ...proxyParams } = params; // throw an error if no model/provider selected (this should usually never be reached, the UI should check this first, but might happen in cases like Apply where we haven't built much UI/checks yet, good practice to have check logic on backend) - const isDisabled = isFeatureNameDisabled(featureName, this.voidSettingsService.state) - const modelSelection = this.voidSettingsService.state.modelSelectionOfFeature[featureName] - if (isDisabled || modelSelection === null) { - let message: string - - if (isDisabled === 'addProvider' || isDisabled === 'providerNotAutoDetected') - message = `Please add a provider in Void's Settings.` - else if (isDisabled === 'addModel') - message = `Please add a model.` - else if (isDisabled === 'needToEnableModel') - message = `Please enable a model.` - else if (isDisabled === 'notFilledIn') - message = `Please fill in Void's Settings${modelSelection !== null ? ` for ${displayInfoOfProviderName(modelSelection.providerName).title}` : ''}.` - else - message = `Please add a provider in Void's Settings.` - + if (modelSelection === null) { + const message = `Please add a provider in Void's Settings.` onError({ message, fullError: null }) return null } - const { providerName, modelName } = modelSelection - // add state for request id const requestId = generateUuid(); this.llmMessageHooks.onText[requestId] = onText @@ -116,17 +98,15 @@ export class LLMMessageService extends Disposable implements ILLMMessageService this.llmMessageHooks.onError[requestId] = onError const { aiInstructions } = this.voidSettingsService.state.globalSettings - const { settingsOfProvider, optionsOfModelSelection, } = this.voidSettingsService.state + const { settingsOfProvider, } = this.voidSettingsService.state // params will be stripped of all its functions over the IPC channel this.channel.call('sendLLMMessage', { ...proxyParams, aiInstructions, requestId, - providerName, - modelName, settingsOfProvider, - optionsOfModelSelection, + modelSelection, } satisfies MainSendLLMMessageParams); return requestId diff --git a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts similarity index 74% rename from src/vs/workbench/contrib/void/common/llmMessageTypes.ts rename to src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 13b078aa..76d62af9 100644 --- a/src/vs/workbench/contrib/void/common/llmMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -3,9 +3,8 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import type { ChatMessage } from '../browser/chatThreadService.js' import type { InternalToolInfo, ToolName } from '../browser/toolsService.js' -import { FeatureName, OptionsOfModelSelection, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' +import { ModelSelection, ModelSelectionOptions, ProviderName, SettingsOfProvider } from './voidSettingsTypes.js' export const errorDetails = (fullError: Error | null): string | null => { @@ -29,12 +28,15 @@ export const getErrorMessage: (error: unknown) => string = (error) => { export type LLMChatMessage = { - role: 'system' | 'user'; + role: 'system'; + content: string; +} | { + role: 'user'; content: string; } | { role: 'assistant', content: string; // text content - rawAnthropicAssistantContent?: RawAnthropicAssistantContent[]; // used for anthropic signing + anthropicReasoning: AnthropicReasoning[] | null; } | { role: 'tool'; content: string; // result @@ -50,31 +52,14 @@ export type ToolCallType = { id: string; } -export type RawAnthropicAssistantContent = { type: 'thinking'; thinking: string; signature: string; } | { type: 'redacted_thinking'; data: string } | { type: 'text', text: string } - +export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any }) export type OnText = (p: { fullText: string; fullReasoning: string }) => void -export type OnFinalMessage = (p: { fullText: string, toolCalls?: ToolCallType[], fullReasoning?: string, rawAnthropicAssistantContent?: RawAnthropicAssistantContent[] }) => void // id is tool_use_id -export type OnError = (p: { message: string, fullError: Error | null }) => void +export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id +export type OnError = (p: { message: string; fullError: Error | null }) => void export type AbortRef = { current: (() => void) | null } -export const toLLMChatMessage = (c: ChatMessage): LLMChatMessage | null => { - if (c.role === 'user') { - return { role: c.role, content: c.content || '(empty message)' } - } - else if (c.role === 'assistant') - return { role: c.role, content: c.content || '(empty message)' } - else if (c.role === 'tool') - return { role: c.role, id: c.id, name: c.name, params: c.paramsStr, content: c.content || '(empty output)' } - else if (c.role === 'tool_request') - return null - else { - throw new Error(`Role ${(c as any).role} not recognized.`) - } -} - - export type LLMFIMMessage = { prefix: string; suffix: string; @@ -97,8 +82,9 @@ export type ServiceSendLLMMessageParams = { onFinalMessage: OnFinalMessage; onError: OnError; logging: { loggingName: string, }; - useProviderFor: FeatureName; -} & SendLLMType + modelSelection: ModelSelection | null; + modelSelectionOptions: ModelSelectionOptions | undefined; +} & SendLLMType; // params to the true sendLLMMessage function export type SendLLMMessageParams = { @@ -110,10 +96,10 @@ export type SendLLMMessageParams = { aiInstructions: string; - providerName: ProviderName; - modelName: string; + modelSelection: ModelSelection; + modelSelectionOptions: ModelSelectionOptions | undefined; + settingsOfProvider: SettingsOfProvider; - optionsOfModelSelection: OptionsOfModelSelection; } & SendLLMType diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 0df1ec3b..278d63da 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -14,8 +14,10 @@ import { IMetricsService } from './metricsService.js'; import { getModelCapabilities } from './modelCapabilities.js'; import { defaultSettingsOfProvider, FeatureName, ProviderName, ModelSelectionOfFeature, SettingsOfProvider, SettingName, providerNames, ModelSelection, modelSelectionsEqual, featureNames, VoidModelInfo, GlobalSettings, GlobalSettingName, defaultGlobalSettings, defaultProviderSettings, ModelSelectionOptions, OptionsOfModelSelection } from './voidSettingsTypes.js'; +// past values: +// 'void.settingsServiceStorage' -const STORAGE_KEY = 'void.settingsServiceStorage' +const STORAGE_KEY = 'void.settingsServiceStorageI' // name is the name in the dropdown @@ -97,7 +99,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection) => boolean; emptyMessage: string | null } } = { 'Autocomplete': { filter: o => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: 'No models support FIM' }, - 'Ctrl+L': { filter: o => true, emptyMessage: null }, + 'Chat': { filter: o => true, emptyMessage: null }, 'Ctrl+K': { filter: o => true, emptyMessage: null }, 'Apply': { filter: o => true, emptyMessage: null }, } @@ -172,7 +174,7 @@ const _validatedState = (state: Omit) => { const defaultState = () => { const d: VoidSettingsState = { settingsOfProvider: deepClone(defaultSettingsOfProvider), - modelSelectionOfFeature: { 'Ctrl+L': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null }, + modelSelectionOfFeature: { 'Chat': null, 'Ctrl+K': null, 'Autocomplete': null, 'Apply': null }, globalSettings: deepClone(defaultGlobalSettings), optionsOfModelSelection: {}, _modelOptions: [], // computed later diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index bda7e3ce..5636790a 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -310,7 +310,7 @@ export const modelSelectionsEqual = (m1: ModelSelection, m2: ModelSelection) => } // this is a state -export const featureNames = ['Ctrl+L', 'Ctrl+K', 'Autocomplete', 'Apply'] as const +export const featureNames = ['Chat', 'Ctrl+K', 'Autocomplete', 'Apply'] as const export type ModelSelectionOfFeature = Record<(typeof featureNames)[number], ModelSelection | null> export type FeatureName = keyof ModelSelectionOfFeature @@ -321,7 +321,7 @@ export const displayInfoOfFeatureName = (featureName: FeatureName) => { else if (featureName === 'Ctrl+K') return 'Quick Edit' // sidebar: - else if (featureName === 'Ctrl+L') + else if (featureName === 'Chat') return 'Chat' else if (featureName === 'Apply') return 'Fast Apply' diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts index 02bf0e0d..6ff7906e 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/preprocessLLMMessages.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { RawAnthropicAssistantContent, LLMChatMessage, LLMFIMMessage } from '../../common/llmMessageTypes.js'; +import { AnthropicReasoning, LLMChatMessage, LLMFIMMessage } from '../../common/sendLLMMessageTypes.js'; import { deepClone } from '../../../../../base/common/objects.js'; @@ -22,8 +22,7 @@ type InternalLLMChatMessage = { content: string; } | { role: 'assistant', - content: string | (RawAnthropicAssistantContent | { type: 'text'; text: string })[]; - rawAnthropicAssistantContent?: RawAnthropicAssistantContent[] | undefined; + content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; } | { role: 'tool'; content: string; // result @@ -33,7 +32,10 @@ type InternalLLMChatMessage = { } -const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }) => { +const EMPTY_MESSAGE = '(empty message)' +const EMPTY_TOOL_CONTENT = '(empty content)' + +const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatMessage[] }): { messages: LLMChatMessage[] } => { const messages = deepClone(messages_) const newMessages: LLMChatMessage[] = [] if (messages.length >= 0) newMessages.push(messages[0]) @@ -41,12 +43,12 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM // remove duplicate roles for (let i = 1; i < messages.length; i += 1) { const curr = messages[i] - const prev = messages[i - 1] - // if found a repeated role, put the current content in the prev - if ((curr.role === 'user' && prev.role === 'user') || (curr.role === 'assistant' && prev.role === 'assistant')) { - prev.content += '\n' + curr.content - continue - } + // const prev = messages[i - 1] + // // if found a repeated role, put the current content in the prev + // if ((curr.role === 'assistant' && prev.role === 'assistant')) { + // prev.content += '\n' + curr.content + // continue + // } // add the message newMessages.push(curr) } @@ -58,29 +60,6 @@ const prepareMessages_normalize = ({ messages: messages_ }: { messages: LLMChatM -// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent -const prepareMessages_anthropicContent = ({ messages, supportsAnthropicContent }: { messages: LLMChatMessage[], supportsAnthropicContent: boolean }) => { - const newMessages: InternalLLMChatMessage[] = [] - for (const m of messages) { - if (m.role !== 'assistant') { - newMessages.push(m) - continue - } - let newMessage: InternalLLMChatMessage - if (supportsAnthropicContent) { - const newContent = m.rawAnthropicAssistantContent - newMessage = { role: 'assistant', content: newContent ?? m.content } - } - else { - newMessage = m - } - delete newMessage.rawAnthropicAssistantContent // important to delete this field - newMessages.push(m) - } - return { messages: newMessages } -} - - // no matter whether the model supports a system message or not (or what format it supports), add it in some way @@ -173,27 +152,27 @@ openai on prompting - https://platform.openai.com/docs/guides/reasoning#advice-o openai on developer system message - https://cdn.openai.com/spec/model-spec-2024-05-08.html#follow-the-chain-of-command */ +type PrepareMessagesToolsOpenAI = ( + Exclude | { + role: 'assistant', + content: string | (AnthropicReasoning | { type: 'text'; text: string })[]; + tool_calls?: { + type: 'function'; + id: string; + function: { + name: string; + arguments: string; + } + }[] + } | { + role: 'tool', + tool_call_id: string; + content: string; + } +)[] const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatMessage[], }) => { - const newMessages: ( - Exclude | { - role: 'assistant', - content: string | object[]; - tool_calls?: { - type: 'function'; - id: string; - function: { - name: string; - arguments: string; - } - }[] - } | { - role: 'tool', - id: string; // old val - tool_call_id: string; // new val - content: string; - } - )[] = []; + const newMessages: PrepareMessagesToolsOpenAI = []; for (let i = 0; i < messages.length; i += 1) { const currMsg = messages[i] @@ -219,9 +198,8 @@ const prepareMessages_tools_openai = ({ messages }: { messages: InternalLLMChatM // add the tool newMessages.push({ role: 'tool', - id: currMsg.id, - content: currMsg.content, tool_call_id: currMsg.id, + content: currMsg.content || EMPTY_TOOL_CONTENT, }) } return { messages: newMessages } @@ -250,34 +228,44 @@ anthropic RESPONSE (role=user): }] */ -const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => { - const newMessages: ( - Exclude | { - role: 'assistant', - content: string | ( - | RawAnthropicAssistantContent - | { - type: 'text'; - text: string; - } - | { - type: 'tool_use'; - name: string; - input: Record; - id: string; - })[] - } | { - role: 'user', - content: string | ({ +type PrepareMessagesToolsAnthropic = ( + Exclude | { + role: 'assistant', + content: string | ( + | AnthropicReasoning + | { type: 'text'; text: string; - } | { - type: 'tool_result'; - tool_use_id: string; - content: string; + } + | { + type: 'tool_use'; + name: string; + input: Record; + id: string; })[] - } - )[] = messages; + } | { + role: 'user', + content: string | ({ + type: 'text'; + text: string; + } | { + type: 'tool_result'; + tool_use_id: string; + content: string; + })[] + } +)[] +/* +Converts: + +assistant: ...content +tool: (id, name, params) +-> +assistant: ...content, call(name, id, params) +user: ...content, result(id, content) +*/ +const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMChatMessage[], }) => { + const newMessages: PrepareMessagesToolsAnthropic = messages; for (let i = 0; i < newMessages.length; i += 1) { @@ -296,7 +284,7 @@ const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMCh newMessages[i] = { role: 'user', content: [ - ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content }] as const, + ...[{ type: 'tool_result', tool_use_id: currMsg.id, content: currMsg.content || EMPTY_TOOL_CONTENT }] as const, ...currMsg.content ? [{ type: 'text', text: currMsg.content }] as const : [], ] } @@ -307,8 +295,9 @@ const prepareMessages_tools_anthropic = ({ messages }: { messages: InternalLLMCh +type PrepareMessagesTools = PrepareMessagesToolsAnthropic | PrepareMessagesToolsOpenAI -const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }) => { +const prepareMessages_tools = ({ messages, supportsTools }: { messages: InternalLLMChatMessage[], supportsTools: false | 'anthropic-style' | 'openai-style' }): { messages: PrepareMessagesTools } => { if (!supportsTools) { return { messages: messages } } @@ -324,6 +313,128 @@ const prepareMessages_tools = ({ messages, supportsTools }: { messages: Internal } +// remove rawAnthropicAssistantContent, and make content equal to it if supportsAnthropicContent +const prepareMessages_anthropicContent = ({ messages, supportsAnthropicReasoningSignature }: { messages: LLMChatMessage[], supportsAnthropicReasoningSignature: boolean }) => { + const newMessages: InternalLLMChatMessage[] = [] + for (const m of messages) { + if (m.role !== 'assistant') { + newMessages.push(m) + continue + } + let newMessage: InternalLLMChatMessage + if (supportsAnthropicReasoningSignature && m.anthropicReasoning) { + const content = m.content ? [...m.anthropicReasoning, { type: 'text' as const, text: m.content }] : m.anthropicReasoning + newMessage = { role: 'assistant', content: content } + } + else { + newMessage = { role: 'assistant', content: m.content } + } + newMessages.push(newMessage) + } + return { messages: newMessages } +} + + + + + +// do this at end +const prepareMessages_noEmptyMessage = ({ messages }: { messages: PrepareMessagesTools }): { messages: PrepareMessagesTools } => { + for (const currMsg of messages) { + + // don't do this for tools + if (currMsg.role === 'tool') continue + + // don't do this for assistant or user messages that have tool_calls or tool_results + const oai = currMsg as PrepareMessagesToolsOpenAI[0] + if (oai.role === 'assistant') { + if (oai.tool_calls) continue + } + const anth = currMsg as PrepareMessagesToolsAnthropic[0] + if (anth.role === 'assistant' || anth.role === 'user') { + if (typeof anth.content !== 'string') { + const hasContent = anth.content.find(c => c.type === 'tool_use' || c.type === 'tool_result') + if (hasContent) continue + } + } + + + if (typeof currMsg.content === 'string') { + currMsg.content = currMsg.content || EMPTY_MESSAGE + } + else { + for (const c of currMsg.content) { + if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE + else if (c.type === 'tool_use') { } + else if (c.type === 'tool_result') { } + } + } + + } + return { messages } +} + + + +// --- CHAT --- + +export const prepareMessages = ({ + messages, + aiInstructions, + supportsSystemMessage, + supportsTools, + supportsAnthropicReasoningSignature, +}: { + messages: LLMChatMessage[], + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', + supportsTools: false | 'anthropic-style' | 'openai-style', + supportsAnthropicReasoningSignature: boolean, +}) => { + const { messages: messages1 } = prepareMessages_normalize({ messages }) + const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicReasoningSignature }) + const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage }) + const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools }) + const { messages: messages5 } = prepareMessages_noEmptyMessage({ messages: messages4 }) + return { + messages: messages5 as any, + separateSystemMessageStr + } as const +} + + + + + + + +// --- FIM --- + +export const prepareFIMMessage = ({ + messages, + aiInstructions, +}: { + messages: LLMFIMMessage, + aiInstructions: string, +}) => { + + let prefix = `\ +${!aiInstructions ? '' : `\ +// Instructions: +// Do not output an explanation. Try to avoid outputting comments. Only output the middle code. +${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`} + +${messages.prefix}` + + const suffix = messages.suffix + const stopTokens = messages.stopTokens + const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const + return ret +} + + + + @@ -357,60 +468,3 @@ gemini response: - - -// --- CHAT --- - -export const prepareMessages = ({ - messages, - aiInstructions, - supportsSystemMessage, - supportsTools, - supportsAnthropicContent, -}: { - messages: LLMChatMessage[], - aiInstructions: string, - supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', - supportsTools: false | 'anthropic-style' | 'openai-style', - supportsAnthropicContent: boolean, -}) => { - const { messages: messages1 } = prepareMessages_normalize({ messages }) - const { messages: messages2 } = prepareMessages_anthropicContent({ messages: messages1, supportsAnthropicContent }) - const { messages: messages3, separateSystemMessageStr } = prepareMessages_systemMessage({ messages: messages2, aiInstructions, supportsSystemMessage }) - const { messages: messages4 } = prepareMessages_tools({ messages: messages3, supportsTools }) - - return { - messages: messages4 as any, - separateSystemMessageStr - } as const -} - - - - - - - -// --- FIM --- - -export const prepareFIMMessage = ({ - messages, - aiInstructions, -}: { - messages: LLMFIMMessage, - aiInstructions: string, -}) => { - - let prefix = `\ -${!aiInstructions ? '' : `\ -// Instructions: -// Do not output an explanation. Try to avoid outputting comments. Only output the middle code. -${aiInstructions.split('\n').map(line => `//${line}`).join('\n')}`} - -${messages.prefix}` - - const suffix = messages.suffix - const stopTokens = messages.stopTokens - const ret = { prefix, suffix, stopTokens, maxTokens: 300 } as const - return ret -} 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 f58eae38..dd75882e 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 @@ -9,9 +9,9 @@ import OpenAI, { ClientOptions } from 'openai'; import { Model as OpenAIModel } from 'openai/resources/models.js'; import { extractReasoningOnFinalMessage, extractReasoningOnTextWrapper } from '../../browser/helpers/extractCodeFromResult.js'; -import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/llmMessageTypes.js'; +import { LLMChatMessage, LLMFIMMessage, ModelListParams, OllamaModelResponse, OnError, OnFinalMessage, OnText } from '../../common/sendLLMMessageTypes.js'; import { InternalToolInfo, isAToolName, ToolName } from '../../browser/toolsService.js'; -import { defaultProviderSettings, displayInfoOfProviderName, OptionsOfModelSelection, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; +import { defaultProviderSettings, displayInfoOfProviderName, ModelSelectionOptions, ProviderName, SettingsOfProvider } from '../../common/voidSettingsTypes.js'; import { prepareFIMMessage, prepareMessages } from './preprocessLLMMessages.js'; import { getModelSelectionState, getModelCapabilities, getProviderCapabilities } from '../../common/modelCapabilities.js'; @@ -23,7 +23,7 @@ type InternalCommonMessageParams = { onError: OnError; providerName: ProviderName; settingsOfProvider: SettingsOfProvider; - optionsOfModelSelection: OptionsOfModelSelection; + modelSelectionOptions: ModelSelectionOptions | undefined; modelName: string; _setAborter: (aborter: () => void) => void; } @@ -140,7 +140,7 @@ const _sendOpenAICompatibleFIM = ({ messages: messages_, onFinalMessage, onError }) .then(async response => { const fullText = response.choices[0]?.text - onFinalMessage({ fullText, }); + onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }); }) .catch(error => { if (error instanceof OpenAI.APIError && error.status === 401) { onError({ message: invalidApiKeyMessage(providerName), fullError: error }); } @@ -168,7 +168,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage const { providerReasoningIOSettings } = getProviderCapabilities(providerName) - const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicContent: false }) // can change supportsAnthropicContent if e.g. OpenRouter starts supporting anthropic extended thinking + const { messages } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: false }) const tools = (supportsTools && ((tools_?.length ?? 0) !== 0)) ? tools_?.map(tool => toOpenAICompatibleTool(tool)) : undefined const includeInPayload = canIOReasoning ? providerReasoningIOSettings?.input?.includeInPayload || {} : {} @@ -222,9 +222,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage else { if (manuallyParseReasoning) { const { fullText, fullReasoning } = extractReasoningOnFinalMessage(fullTextSoFar, openSourceThinkTags) - onFinalMessage({ fullText, fullReasoning, toolCalls }); + onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning: null }); } else { - onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls }); + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, toolCalls, anthropicReasoning: null }); } } }) @@ -280,7 +280,7 @@ const toAnthropicTool = (toolInfo: InternalToolInfo) => { } satisfies Anthropic.Messages.Tool } -const toolCallsFrom_AnthropicContent = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => { +const toolCallsFrom_Anthropic = (content: Anthropic.Messages.ContentBlock[]): ToolCallsFrom_ReturnType => { return content.map(c => { if (c.type !== 'tool_use') return null if (!isAToolName(c.name)) return null @@ -288,7 +288,7 @@ const toolCallsFrom_AnthropicContent = (content: Anthropic.Messages.ContentBlock }).filter(t => !!t) } -const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { +const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName: modelName_, _setAborter, aiInstructions, tools: tools_ }: SendChatParams_Internal) => { const { modelName, supportsSystemMessage, @@ -299,9 +299,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM const { isReasoningEnabled, reasoningBudget, - } = getModelSelectionState(providerName, modelName_, optionsOfModelSelection) // user's modelName_ here + } = getModelSelectionState(providerName, modelName_, modelSelectionOptions) // user's modelName_ here - const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicContent: true }) + const { messages, separateSystemMessageStr } = prepareMessages({ messages: messages_, aiInstructions, supportsSystemMessage, supportsTools, supportsAnthropicReasoningSignature: true }) const thisConfig = settingsOfProvider.anthropic const anthropic = new Anthropic({ apiKey: thisConfig.apiKey, dangerouslyAllowBrowser: true }); @@ -370,8 +370,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM // on done - (or when error/fail) - this is called AFTER last streamEvent stream.on('finalMessage', (response) => { - const toolCalls = toolCallsFrom_AnthropicContent(response.content) - onFinalMessage({ fullText, fullReasoning, toolCalls, rawAnthropicAssistantContent: response.content as any }) + const toolCalls = toolCallsFrom_Anthropic(response.content) + const anthropicReasoning = response.content.filter(c => c.type === 'thinking' || c.type === 'redacted_thinking') + onFinalMessage({ fullText, fullReasoning, toolCalls, anthropicReasoning }) }) // on error stream.on('error', (error) => { @@ -455,7 +456,7 @@ const sendOllamaFIM = ({ messages: messages_, onFinalMessage, onError, settingsO const newText = chunk.response fullText += newText } - onFinalMessage({ fullText }) + onFinalMessage({ fullText, fullReasoning: '', anthropicReasoning: null }) }) // when error/fail .catch((error) => { @@ -494,6 +495,11 @@ export const sendLLMMessageToProviderImplementation = { sendFIM: null, list: null, }, + // mistral: { + // sendChat: , // TODO + // sendFIM: , // TODO // https://docs.mistral.ai/api/#tag/fim + // list: null, + // }, ollama: { sendChat: (params) => _sendOpenAICompatibleChat(params), sendFIM: sendOllamaFIM, diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 0c51bae2..1e17d97b 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -3,7 +3,7 @@ * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. *--------------------------------------------------------------------------------------*/ -import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/llmMessageTypes.js'; +import { SendLLMMessageParams, OnText, OnFinalMessage, OnError } from '../../common/sendLLMMessageTypes.js'; import { IMetricsService } from '../../common/metricsService.js'; import { displayInfoOfProviderName } from '../../common/voidSettingsTypes.js'; import { sendLLMMessageToProviderImplementation } from './sendLLMMessage.impl.js'; @@ -19,9 +19,8 @@ export const sendLLMMessage = ({ abortRef: abortRef_, logging: { loggingName }, settingsOfProvider, - optionsOfModelSelection, - providerName, - modelName, + modelSelection, + modelSelectionOptions, tools, }: SendLLMMessageParams, @@ -29,6 +28,8 @@ export const sendLLMMessage = ({ ) => { + const { providerName, modelName } = modelSelection + // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureLLMEvent = (eventId: string, extras?: object) => { metricsService.capture(eventId, { @@ -105,18 +106,19 @@ export const sendLLMMessage = ({ } const { sendFIM, sendChat } = implementation if (messagesType === 'chatMessages') { - sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName, _setAborter, providerName, aiInstructions, tools }) + sendChat({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions, tools }) return } if (messagesType === 'FIMMessage') { if (sendFIM) { - sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, optionsOfModelSelection, modelName, _setAborter, providerName, aiInstructions }) + sendFIM({ messages: messages_, onText, onFinalMessage, onError, settingsOfProvider, modelSelectionOptions, modelName, _setAborter, providerName, aiInstructions }) return } onError({ message: `Error: This provider does not support Autocomplete yet.`, fullError: null }) return } onError({ message: `Error: Message type "${messagesType}" not recognized.`, fullError: null }) + return } catch (error) { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts b/src/vs/workbench/contrib/void/electron-main/sendLLMMessageChannel.ts similarity index 99% rename from src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts rename to src/vs/workbench/contrib/void/electron-main/sendLLMMessageChannel.ts index c1b5ed13..12b0d984 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessageChannel.ts +++ b/src/vs/workbench/contrib/void/electron-main/sendLLMMessageChannel.ts @@ -8,7 +8,7 @@ import { IServerChannel } from '../../../../base/parts/ipc/common/ipc.js'; import { Emitter, Event } from '../../../../base/common/event.js'; -import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/llmMessageTypes.js'; +import { EventLLMMessageOnTextParams, EventLLMMessageOnErrorParams, EventLLMMessageOnFinalMessageParams, MainSendLLMMessageParams, AbortRef, SendLLMMessageParams, MainLLMMessageAbortParams, ModelListParams, EventModelListOnSuccessParams, EventModelListOnErrorParams, OllamaModelResponse, VLLMModelResponse, MainModelListParams, } from '../common/sendLLMMessageTypes.js'; import { sendLLMMessage } from './llmMessage/sendLLMMessage.js' import { IMetricsService } from '../common/metricsService.js'; import { sendLLMMessageToProviderImplementation } from './llmMessage/sendLLMMessage.impl.js';