diff --git a/build/gulpfile.vscode.win32.js b/build/gulpfile.vscode.win32.js index 311299c6..98175f53 100644 --- a/build/gulpfile.vscode.win32.js +++ b/build/gulpfile.vscode.win32.js @@ -82,7 +82,6 @@ function buildWin32Setup(arch, target) { productJson['target'] = target; fs.writeFileSync(productJsonPath, JSON.stringify(productJson, undefined, '\t')); - console.log('RawVersion!!!!!!!!!!!!!!', pkg.version.replace(/-\w+$/, '')) // Void const quality = product.quality || 'dev'; const definitions = { NameLong: product.nameLong, diff --git a/package-lock.json b/package-lock.json index 513936cc..a365be59 100644 --- a/package-lock.json +++ b/package-lock.json @@ -13,7 +13,7 @@ "@anthropic-ai/sdk": "^0.40.0", "@c4312/eventsource-umd": "^3.0.5", "@floating-ui/react": "^0.27.8", - "@google/generative-ai": "^0.24.0", + "@google/generative-ai": "^0.24.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", @@ -1817,9 +1817,9 @@ "license": "MIT" }, "node_modules/@google/generative-ai": { - "version": "0.24.0", - "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.0.tgz", - "integrity": "sha512-fnEITCGEB7NdX0BhoYZ/cq/7WPZ1QS5IzJJfC3Tg/OwkvBetMiVJciyaan297OvE4B9Jg1xvo0zIazX/9sGu1Q==", + "version": "0.24.1", + "resolved": "https://registry.npmjs.org/@google/generative-ai/-/generative-ai-0.24.1.tgz", + "integrity": "sha512-MqO+MLfM6kjxcKoy0p1wRzG3b4ZZXtPI+z2IE26UogS2Cm/XHO+7gGRBh6gcJsOiIVoH93UwKvW4HdgiOZCy9Q==", "license": "Apache-2.0", "engines": { "node": ">=18.0.0" diff --git a/package.json b/package.json index 23de71cf..462cf549 100644 --- a/package.json +++ b/package.json @@ -75,7 +75,7 @@ "@anthropic-ai/sdk": "^0.40.0", "@c4312/eventsource-umd": "^3.0.5", "@floating-ui/react": "^0.27.8", - "@google/generative-ai": "^0.24.0", + "@google/generative-ai": "^0.24.1", "@microsoft/1ds-core-js": "^3.2.13", "@microsoft/1ds-post-js": "^3.2.13", "@mistralai/mistralai": "^1.6.0", diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index a2d5dbd7..1dd0d4af 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -235,6 +235,7 @@ export interface IChatThreadService { isCurrentlyFocusingMessage(): boolean; setCurrentlyFocusedMessageIdx(messageIdx: number | undefined): void; + popStagingSelections(numPops?: number): void; addNewStagingSelection(newSelection: StagingSelectionItem): void; dangerousSetState: (newState: ThreadsState) => void; @@ -1096,7 +1097,6 @@ We only need to do it for files that were edited since `from`, ie files between // interrupt existing stream if (this.streamState[threadId]?.isRunning) { - console.log('stopping....') await this.abortRunning(threadId) } @@ -1612,6 +1612,31 @@ We only need to do it for files that were edited since `from`, ie files between } + // Pops the staging selections from the current thread's state + popStagingSelections(numPops: number): void { + + numPops = numPops ?? 1; + + const focusedMessageIdx = this.getCurrentFocusedMessageIdx() + + // set the selections to the proper value + let selections: StagingSelectionItem[] = [] + let setSelections = (s: StagingSelectionItem[]) => { } + + if (focusedMessageIdx === undefined) { + selections = this.getCurrentThreadState().stagingSelections + setSelections = (s: StagingSelectionItem[]) => this.setCurrentThreadState({ stagingSelections: s }) + } else { + selections = this.getCurrentMessageState(focusedMessageIdx).stagingSelections + setSelections = (s) => this.setCurrentMessageState(focusedMessageIdx, { stagingSelections: s }) + } + + setSelections([ + ...selections.slice(0, selections.length - numPops) + ]) + + } + // set message.state private _setCurrentMessageState(state: Partial, messageIdx: number): void { diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 65b45d33..56b66e02 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -8,9 +8,9 @@ import { IEditorService } from '../../../services/editor/common/editorService.js import { ChatMessage } from '../common/chatThreadServiceTypes.js'; import { getIsReasoningEnabledState, getMaxOutputTokens, getModelCapabilities } from '../common/modelCapabilities.js'; import { reParsedToolXMLString, chat_systemMessage, ToolName } from '../common/prompt/prompts.js'; -import { AnthropicLLMChatMessage, AnthropicReasoning, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; +import { AnthropicLLMChatMessage, AnthropicReasoning, GeminiLLMChatMessage, LLMChatMessage, LLMFIMMessage, OpenAILLMChatMessage, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { ChatMode, FeatureName, ModelSelection } from '../common/voidSettingsTypes.js'; +import { ChatMode, FeatureName, ModelSelection, ProviderName } from '../common/voidSettingsTypes.js'; import { IDirectoryStrService } from './directoryStrService.js'; import { ITerminalToolService } from './terminalToolService.js'; import { IVoidModelService } from '../common/voidModelService.js'; @@ -36,8 +36,6 @@ type SimpleLLMMessage = { } - - const EMPTY_MESSAGE = '(empty message)' const CHARS_PER_TOKEN = 4 @@ -69,7 +67,7 @@ openai on developer system message - https://cdn.openai.com/spec/model-spec-2024 */ -const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): LLMChatMessage[] => { +const prepareMessages_openai_tools = (messages: SimpleLLMMessage[]): AnthropicOrOpenAILLMMessage[] => { const newMessages: OpenAILLMChatMessage[] = []; @@ -136,8 +134,9 @@ assistant: ...content, call(name, id, params) user: ...content, result(id, content) */ +type AnthropicOrOpenAILLMMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage -const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => { +const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => { const newMessages: (AnthropicLLMChatMessage | (SimpleLLMMessage & { role: 'tool' }))[] = messages; for (let i = 0; i < messages.length; i += 1) { @@ -195,9 +194,9 @@ const prepareMessages_anthropic_tools = (messages: SimpleLLMMessage[], supportsA } -const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): LLMChatMessage[] => { +const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthropicReasoning: boolean): AnthropicOrOpenAILLMMessage[] => { - const llmChatMessages: LLMChatMessage[] = []; + const llmChatMessages: AnthropicOrOpenAILLMMessage[] = []; for (let i = 0; i < messages.length; i += 1) { const c = messages[i] @@ -206,7 +205,7 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop if (c.role === 'assistant') { // if called a tool (message after it), re-add its XML to the message // alternatively, could just hold onto the original output, but this way requires less piping raw strings everywhere - let content: LLMChatMessage['content'] = c.content + let content: AnthropicOrOpenAILLMMessage['content'] = c.content if (next?.role === 'tool') { content = `${content}\n\n${reParsedToolXMLString(next.name, next.rawParams)}` } @@ -239,24 +238,20 @@ const prepareMessages_XML_tools = (messages: SimpleLLMMessage[], supportsAnthrop -const prepareMessages_providerSpecific = (messages: SimpleLLMMessage[], specialToolFormat: 'openai-style' | 'anthropic-style' | undefined, supportsAnthropicReasoning: boolean): LLMChatMessage[] => { - const llmChatMessages: LLMChatMessage[] = [] - if (!specialToolFormat) { // XML tool behavior - return prepareMessages_XML_tools(messages, supportsAnthropicReasoning) - } - else if (specialToolFormat === 'anthropic-style') { - return prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning) - } - else if (specialToolFormat === 'openai-style') { - return prepareMessages_openai_tools(messages) - } - return llmChatMessages -} + +export type GeminiMessage = { + role: 'user' | 'model'; // Gemini uses 'user' and 'model' roles + parts: ( + | { text: string; } + | { functionCall: { tool_call: any } } + | { functionResponse: { name: ToolName, response: { result: string } } } + )[]; +}; // --- CHAT --- -const prepareMessages = ({ +const prepareOpenAIOrAnthropicMessages = ({ messages, systemMessage, aiInstructions, @@ -274,7 +269,7 @@ const prepareMessages = ({ supportsAnthropicReasoning: boolean, contextWindow: number, maxOutputTokens: number | null | undefined, -}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => { +}): { messages: AnthropicOrOpenAILLMMessage[], separateSystemMessage: string | undefined } => { maxOutputTokens = maxOutputTokens ?? 4_096 // default to 4096 // ================ trim ================ @@ -350,7 +345,19 @@ const prepareMessages = ({ } // ================ tools and anthropicReasoning ================ - const llmMessages: LLMChatMessage[] = prepareMessages_providerSpecific(messages, specialToolFormat, supportsAnthropicReasoning) + + let llmChatMessages: AnthropicOrOpenAILLMMessage[] = [] + if (!specialToolFormat) { // XML tool behavior + llmChatMessages = prepareMessages_XML_tools(messages, supportsAnthropicReasoning) + } + else if (specialToolFormat === 'anthropic-style') { + llmChatMessages = prepareMessages_anthropic_tools(messages, supportsAnthropicReasoning) + } + else if (specialToolFormat === 'openai-style') { + llmChatMessages = prepareMessages_openai_tools(messages) + } + const llmMessages = llmChatMessages + // ================ system message concat ================ @@ -406,9 +413,83 @@ const prepareMessages = ({ +type GeminiUserPart = (GeminiLLMChatMessage & { role: 'user' })['parts'][0] +type GeminiModelPart = (GeminiLLMChatMessage & { role: 'model' })['parts'][0] +const prepareGeminiMessages = (messages: AnthropicLLMChatMessage[]) => { + let latestToolName: ToolName | undefined = undefined + const messages2: GeminiLLMChatMessage[] = messages.map((m): GeminiLLMChatMessage | null => { + if (m.role === 'assistant') { + if (typeof m.content === 'string') { + return { role: 'model', parts: [{ text: m.content }] } + } + else { + const parts: GeminiModelPart[] = m.content.map((c): GeminiModelPart | null => { + if (c.type === 'text') { + return { text: c.text } + } + else if (c.type === 'tool_use') { + latestToolName = c.name as ToolName + return { functionCall: { name: c.name as ToolName, args: c.input } } + } + else return null + }).filter(m => !!m) + return { role: 'model', parts, } + } + } + else if (m.role === 'user') { + if (typeof m.content === 'string') { + return { role: 'user', parts: [{ text: m.content }] } satisfies GeminiLLMChatMessage + } + else { + const parts: GeminiUserPart[] = m.content.map((c): GeminiUserPart | null => { + if (c.type === 'text') { + return { text: c.text } + } + else if (c.type === 'tool_result') { + if (!latestToolName) return null + return { functionResponse: { name: latestToolName, response: { result: c.content } } } + } + else return null + }).filter(m => !!m) + return { role: 'user', parts, } + } + + } + else return null + }).filter(m => !!m) + + return messages2 +} +const prepareMessages = (params: { + messages: SimpleLLMMessage[], + systemMessage: string, + aiInstructions: string, + supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated', + specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined, + supportsAnthropicReasoning: boolean, + contextWindow: number, + maxOutputTokens: number | null | undefined, + providerName: ProviderName +}): { messages: LLMChatMessage[], separateSystemMessage: string | undefined } => { + const specialFormat = params.specialToolFormat // this is just for ts idiocy + if (params.providerName === 'gemini') { + // treat as anthropic style, then convert to gemini style + const res = prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat === 'gemini-style' ? 'anthropic-style' : undefined }) + const messages = res.messages as AnthropicLLMChatMessage[] + const messages2 = prepareGeminiMessages(messages) + return { messages: messages2, separateSystemMessage: res.separateSystemMessage } + } + else { + if (specialFormat === 'gemini-style') { + throw new Error(`Tried preparing messages with tool format ${params.specialToolFormat} but the provider was ${params.providerName}, not Gemini.`) + } + } + + return prepareOpenAIOrAnthropicMessages({ ...params, specialToolFormat: specialFormat }) +} @@ -418,7 +499,6 @@ export interface IConvertToLLMMessageService { prepareLLMSimpleMessages: (opts: { simpleMessages: SimpleLLMMessage[], systemMessage: string, modelSelection: ModelSelection | null, featureName: FeatureName }) => { messages: LLMChatMessage[], separateSystemMessage: string | undefined } prepareLLMChatMessages: (opts: { chatMessages: ChatMessage[], chatMode: ChatMode, modelSelection: ModelSelection | null }) => Promise<{ messages: LLMChatMessage[], separateSystemMessage: string | undefined }> prepareFIMMessage(opts: { messages: LLMFIMMessage, }): { prefix: string, suffix: string, stopTokens: string[] } - } export const IConvertToLLMMessageService = createDecorator('ConvertToLLMMessageService'); @@ -470,7 +550,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess // system message - private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | undefined) => { + private _generateChatMessagesSystemMessage = async (chatMode: ChatMode, specialToolFormat: 'openai-style' | 'anthropic-style' | 'gemini-style' | undefined) => { const workspaceFolders = this.workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) const openedURIs = this.modelService.getModels().filter(m => m.isAttachedToEditor()).map(m => m.uri.fsPath) || []; @@ -551,6 +631,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess supportsAnthropicReasoning: providerName === 'anthropic', contextWindow, maxOutputTokens, + providerName, }) return { messages, separateSystemMessage }; } @@ -582,6 +663,7 @@ class ConvertToLLMMessageService extends Disposable implements IConvertToLLMMess supportsAnthropicReasoning: providerName === 'anthropic', contextWindow, maxOutputTokens, + providerName, }) return { messages, separateSystemMessage }; } diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 240ce9a4..a106e601 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -14,8 +14,6 @@ import { ICodeEditorService } from '../../../../editor/browser/services/codeEdit import { findDiffs } from './helpers/findDiffs.js'; import { EndOfLinePreference, IModelDecorationOptions, ITextModel } from '../../../../editor/common/model.js'; import { IRange } from '../../../../editor/common/core/range.js'; -import { registerColor } from '../../../../platform/theme/common/colorUtils.js'; -import { Color, RGBA } from '../../../../base/common/color.js'; import { IModelService } from '../../../../editor/common/services/model.js'; import { IUndoRedoElement, IUndoRedoService, UndoRedoElementType } from '../../../../platform/undoRedo/common/undoRedo.js'; import { RenderOptions } from '../../../../editor/browser/widget/diffEditor/components/diffEditorViewZones/renderLines.js'; @@ -25,7 +23,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, } from '../common/prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, tripleTick, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -48,27 +46,6 @@ import { IConvertToLLMMessageService } from './convertToLLMMessageService.js'; // import { isMacintosh } from '../../../../base/common/platform.js'; // import { VOID_OPEN_SETTINGS_ACTION_ID } from './voidSettingsPane.js'; -const configOfBG = (color: Color) => { - return { dark: color, light: color, hcDark: color, hcLight: color, } -} -// gets converted to --vscode-void-greenBG, see void.css, asCssVariable -const greenBG = new Color(new RGBA(155, 185, 85, .2)); // default is RGBA(155, 185, 85, .2) -registerColor('void.greenBG', configOfBG(greenBG), '', true); - -const redBG = new Color(new RGBA(255, 0, 0, .2)); // default is RGBA(255, 0, 0, .2) -registerColor('void.redBG', configOfBG(redBG), '', true); - -const sweepBG = new Color(new RGBA(100, 100, 100, .2)); -registerColor('void.sweepBG', configOfBG(sweepBG), '', true); - -const highlightBG = new Color(new RGBA(100, 100, 100, .1)); -registerColor('void.highlightBG', configOfBG(highlightBG), '', true); - -const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5)); -registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); - - - const numLinesOfStr = (str: string) => str.split('\n').length @@ -129,10 +106,10 @@ const removeWhitespaceExceptNewlines = (str: string): string => { // finds block.orig in fileContents and return its range in file // startingAtLine is 1-indexed and inclusive -const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, startingAtLine?: number) => { +const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveWhitespace: boolean, opts: { startingAtLine?: number, returnType: 'lines' | 'indices' }) => { - const startLineIdx = (fileContents: string) => startingAtLine !== undefined ? - fileContents.split('\n').slice(0, startingAtLine).join('\n').length // num characters in all lines before startingAtLine + const startLineIdx = (fileContents: string) => opts?.startingAtLine !== undefined ? + fileContents.split('\n').slice(0, opts.startingAtLine).join('\n').length // num characters in all lines before startingAtLine : 0 // idx = starting index in fileContents @@ -148,10 +125,18 @@ const findTextInCode = (text: string, fileContents: string, canFallbackToRemoveW if (idx === -1) return 'Not found' as const const lastIdx = fileContents.lastIndexOf(text) if (lastIdx !== idx) return 'Not unique' as const - const startLine = fileContents.substring(0, idx).split('\n').length - const numLines = numLinesOfStr(text) - const endLine = startLine + numLines - 1 - return [startLine, endLine] as const + + if (opts.returnType === 'lines') { + const startLine = fileContents.substring(0, idx).split('\n').length + const numLines = numLinesOfStr(text) + const endLine = startLine + numLines - 1 + return [startLine, endLine] as const + } + + else if (opts.returnType === 'indices') { + return [idx, idx + text.length] as const + } + else throw new Error(`findTextInCode: Invalid returnType ${opts.returnType}`) } @@ -1185,8 +1170,19 @@ class EditCodeService extends Disposable implements IEditCodeService { } - this._instantlyApplySRBlocks(uri, searchReplaceBlocks) + const onError = (e: { message: string; fullError: Error | null; }) => { + // this._notifyError(e) + onDone() + this._undoHistory(uri) + throw e.fullError || new Error(e.message) + } + try { + this._instantlyApplySRBlocks(uri, searchReplaceBlocks) + } + catch (e) { + onError({ message: e + '', fullError: null }) + } onDone() } @@ -1446,7 +1442,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // this._notifyError(e) onDone() this._undoHistory(uri) - throw e.fullError + throw e.fullError || new Error(e.message) } const extractText = (fullText: string, recentlyAddedTextLen: number) => { @@ -1562,23 +1558,16 @@ class EditCodeService extends Disposable implements IEditCodeService { private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { + const problematicCode = `${tripleTick[0]}\n${JSON.stringify(blockOrig)}\n${tripleTick[1]}` + const descStr = str === `Not found` ? - `The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + `The edit was not applied. The text in ORIGINAL must EXACTLY match lines of code in the file, but there was no match for:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code matches a code excerpt exactly.` : str === `Not unique` ? - `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + `The edit was not applied. The text in ORIGINAL must be unique, but the following ORIGINAL code appears multiple times in the file:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code is unique.` : str === 'Has overlap' ? - `The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + `The edit was not applied. The text in the ORIGINAL blocks must not overlap, but the following ORIGINAL code had overlap with another ORIGINAL string:\n${problematicCode}. Ensure you have the latest version of the file, and ensure the ORIGINAL code blocks do not overlap.` : `` - - // string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has - // const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n') - // const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : '' - // const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : '' - // const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}` - const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.' - const errMsg = `${descStr}\n${soFarStr}` - return errMsg - + return descStr } @@ -1590,14 +1579,13 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`) const modelStr = model.getValue(EndOfLinePreference.LF) + const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = [] for (const b of blocks) { - const i = modelStr.indexOf(b.orig) - if (i === -1) - throw new Error(this._errContentOfInvalidStr('Not found', b.orig)) - const j = modelStr.lastIndexOf(b.orig) - if (i !== j) - throw new Error(this._errContentOfInvalidStr('Not unique', b.orig)) + const res = findTextInCode(b.orig, modelStr, true, { returnType: 'indices' }) + if (typeof res === 'string') + throw new Error(this._errContentOfInvalidStr(res, b.orig)) + const [i, _] = res replacements.push({ origStart: i, @@ -1714,7 +1702,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // this._notifyError(e) onDone() this._undoHistory(uri) - throw e.fullError || new Error(e.message) // throw error h + throw e.fullError || new Error(e.message) } // refresh now in case onText takes a while to get 1st message @@ -1768,7 +1756,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // update stream state to the first line of original if some portion of original has been written if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line - const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine) + const originalRange = findTextInCode(block.orig, originalFileCode, false, { startingAtLine, returnType: 'lines' }) if (typeof originalRange !== 'string') { const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) diffZone._streamState.line = startLine @@ -1794,7 +1782,7 @@ class EditCodeService extends Disposable implements IEditCodeService { // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it if (!(blockNum in addedTrackingZoneOfBlockNum)) { - const originalBounds = findTextInCode(block.orig, originalFileCode, true) + const originalBounds = findTextInCode(block.orig, originalFileCode, true, { returnType: 'lines' }) // if error // Check for overlap with existing modified ranges const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => { @@ -1813,9 +1801,10 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('block.orig:', block.orig) console.log('---------') const content = this._errContentOfInvalidStr(errorMessage, block.orig) + const retryMsg = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.' messages.push( { role: 'assistant', content: fullText }, // latest output - { role: 'user', content: content } // user explanation of what's wrong + { role: 'user', content: content + '\n' + retryMsg } // user explanation of what's wrong ) // REVERT ALL BLOCKS 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 d0188f37..23e5ead3 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 @@ -292,7 +292,7 @@ export const VoidChatArea: React.FC = ({ className = '', showModelDropdown = true, showSelections = false, - showProspectiveSelections = true, + showProspectiveSelections = false, selections, setSelections, featureName, @@ -314,11 +314,6 @@ export const VoidChatArea: React.FC = ({ onClick={(e) => { onClickAnywhere?.() }} - onKeyDown={(e: React.KeyboardEvent) => { - if (e.key === 'Escape' && isStreaming && onAbort) { - onAbort(); - } - }} > {/* Selections section */} {showSelections && selections && setSelections && ( @@ -727,7 +722,7 @@ const ToolHeaderWrapper = ({ return (
{/* header */} -
+
{/* left */}
>[0] & { content: string }) => { const accessor = useAccessor() - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) @@ -832,55 +827,41 @@ const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters - } + const applyBoxId = getApplyBoxId({ + threadId: threadId, + messageIdx: messageIdx, + tokenIdx: 'N/A', + }) + componentParams.desc2 = // add children - if (toolMessage.type !== 'tool_error') { + componentParams.children = + + + + if (toolMessage.type === 'success' || toolMessage.type === 'rejected') { const { result } = toolMessage - - componentParams.bottomChildren = - - componentParams.children = - - + componentParams.bottomChildren = + {result?.lintErrors?.map((error, i) => ( +
Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}
+ ))} +
} - else { + else if (toolMessage.type === 'tool_error') { // error const { result } = toolMessage - if (params) { - componentParams.children = - {/* error */} - - {result} - - - {/* content */} - - - } - else { - componentParams.children = + componentParams.bottomChildren = + {result} - } + } } @@ -1063,87 +1044,87 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr setSelections={setStagingSelections} > setIsDisabled(!text)} - onFocus={() => { - setIsFocused(true) - chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); - }} - onBlur={() => { - setIsFocused(false) - }} - onKeyDown={onKeyDown} - fnsRef={textAreaFnsRef} - multiline={true} - /> - -} + enableAtToMention + ref={setTextAreaRef} + className='min-h-[81px] max-h-[500px] px-0.5' + placeholder="Edit your message..." + onChangeText={(text) => setIsDisabled(!text)} + onFocus={() => { + setIsFocused(true) + chatThreadsService.setCurrentlyFocusedMessageIdx(messageIdx); + }} + onBlur={() => { + setIsFocused(false) + }} + onKeyDown={onKeyDown} + fnsRef={textAreaFnsRef} + multiline={true} + /> + + } -const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 + const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1 -return
setIsHovered(true)} - onMouseLeave={() => setIsHovered(false)} -> -
setIsHovered(true)} + onMouseLeave={() => setIsHovered(false)} + > +
{ if (mode === 'display') { onOpenEdit() } }} - > - {chatbubbleContents} -
+ onClick={() => { if (mode === 'display') { onOpenEdit() } }} + > + {chatbubbleContents} +
-
- + { - if (mode === 'display') { - onOpenEdit() - } else if (mode === 'edit') { - onCloseEdit() - } - }} - /> -
+ onClick={() => { + if (mode === 'display') { + onOpenEdit() + } else if (mode === 'edit') { + onCloseEdit() + } + }} + /> +
-
+
} const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => { -return
- {children} -
+ {children} +
} const ProseWrapper = ({ children }: { children: React.ReactNode }) => { -return
- {children} -
+ > + {children} +
} const AssistantMessageComponent = ({ chatMessage, isCheckpointGhost, isCommitted, messageIdx }: { chatMessage: ChatMessage & { role: 'assistant' }, isCheckpointGhost: boolean, messageIdx: number, isCommitted: boolean }) => { -const accessor = useAccessor() -const chatThreadsService = accessor.get('IChatThreadService') + const accessor = useAccessor() + const chatThreadsService = accessor.get('IChatThreadService') -const reasoningStr = chatMessage.reasoning?.trim() || null -const hasReasoning = !!reasoningStr -const isDoneReasoning = !!chatMessage.displayContent -const thread = chatThreadsService.getCurrentThread() + const reasoningStr = chatMessage.reasoning?.trim() || null + const hasReasoning = !!reasoningStr + const isDoneReasoning = !!chatMessage.displayContent + const thread = chatThreadsService.getCurrentThread() -const chatMessageLocation: ChatMessageLocation = { - threadId: thread.id, - messageIdx: messageIdx, -} + const chatMessageLocation: ChatMessageLocation = { + threadId: thread.id, + messageIdx: messageIdx, + } -const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning -if (isEmpty) return null + const isEmpty = !chatMessage.displayContent && !chatMessage.reasoning + if (isEmpty) return null -return <> - {/* reasoning token */} - {hasReasoning && -
- - - - - -
- } + return <> + {/* reasoning token */} + {hasReasoning && +
+ + + + + +
+ } - {/* assistant message */} - {chatMessage.displayContent && -
- - - -
- } - + {/* assistant message */} + {chatMessage.displayContent && +
+ + + +
+ } + } const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneReasoning: boolean, isStreaming: boolean, children: React.ReactNode }) => { -const isDone = isDoneReasoning || !isStreaming -const isWriting = !isDone -const [isOpen, setIsOpen] = useState(isWriting) -useEffect(() => { - if (!isWriting) setIsOpen(false) // if just finished reasoning, close -}, [isWriting]) -return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> - -
- {children} -
-
-
+ const isDone = isDoneReasoning || !isStreaming + const isWriting = !isDone + const [isOpen, setIsOpen] = useState(isWriting) + useEffect(() => { + if (!isWriting) setIsOpen(false) // if just finished reasoning, close + }, [isWriting]) + return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> + +
+ {children} +
+
+
} @@ -1309,10 +1290,10 @@ return : // should either be past or "-ing" tense, not present tense. Eg. when the LLM searches for something, the user expects it to say "I searched for X" or "I am searching for X". Not "I search X". const loadingTitleWrapper = (item: React.ReactNode): React.ReactNode => { -return - {item} - - + return + {item} + + } const titleOfToolName = { @@ -1422,7 +1403,7 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName const toolParams = _toolParams as ToolCallParams['run_command'] return { desc1: `"${toolParams.command}"`, - } + } }, 'run_persistent_command': () => { const toolParams = _toolParams as ToolCallParams['run_persistent_command'] @@ -1577,8 +1558,8 @@ const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => {
} -const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => { - if (lintErrors.length === 0) return null; +const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => { + if (!children) return null; const [isOpen, setIsOpen] = useState(false); return (
@@ -1590,15 +1571,13 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => - Lint errors + {title}
-
- {lintErrors.map((error, i) => ( -
Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}
- ))} +
+ {children}
@@ -1659,7 +1638,7 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ const terminalToolsService = accessor.get('ITerminalToolService') const toolsService = accessor.get('IToolsService') const terminalService = accessor.get('ITerminalService') - const isError = toolMessage.type === 'tool_error' + const isError = false const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) const icon = null @@ -1729,9 +1708,11 @@ const CommandTool = ({ toolMessage, type, threadId }: { threadId: string } & ({ } else if (toolMessage.type === 'tool_error') { const { result } = toolMessage - componentParams.children = - {result} - + componentParams.bottomChildren = + + {result} + + } else if (toolMessage.type === 'running_now') { componentParams.children =
@@ -1760,7 +1741,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } @@ -1783,11 +1764,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.desc2 = - componentParams.children = + componentParams.bottomChildren = {result} - + } return @@ -1805,7 +1786,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } @@ -1830,11 +1811,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else { const { result } = toolMessage - componentParams.children = + componentParams.bottomChildren = {result} - + } return @@ -1853,7 +1834,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } @@ -1885,11 +1866,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else { const { result } = toolMessage - componentParams.children = + componentParams.bottomChildren = {result} - + } return @@ -1899,7 +1880,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) @@ -1934,11 +1915,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else { const { result } = toolMessage - componentParams.children = + componentParams.bottomChildren = {result} - + } return @@ -1948,7 +1929,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) @@ -1989,11 +1970,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else { const { result } = toolMessage - componentParams.children = + componentParams.bottomChildren = {result} - + } return } @@ -2004,7 +1985,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const accessor = useAccessor(); const toolsService = accessor.get('IToolsService'); const title = getTitle(toolMessage); - const isError = toolMessage.type === 'tool_error'; + const isError = false const isRejected = toolMessage.type === 'rejected' const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor); const icon = null; @@ -2033,13 +2014,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage; - componentParams.children = + componentParams.bottomChildren = {result} - ; + } return ; @@ -2060,7 +2041,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } @@ -2079,11 +2060,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, else if (toolMessage.type === 'tool_error') { const { result } = toolMessage if (params) componentParams.desc2 = - componentParams.children = + componentParams.bottomChildren = {result} - + } return @@ -2096,7 +2077,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) @@ -2118,11 +2099,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, else if (toolMessage.type === 'tool_error') { const { result } = toolMessage if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - componentParams.children = componentParams.children = + componentParams.bottomChildren = {result} - + } else if (toolMessage.type === 'running_now') { // nothing more is needed @@ -2139,7 +2120,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isFolder = toolMessage.params?.isFolder ?? false - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const title = getTitle(toolMessage) const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor) @@ -2160,11 +2141,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, else if (toolMessage.type === 'tool_error') { const { result } = toolMessage if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } - componentParams.children = componentParams.children = + componentParams.children = componentParams.bottomChildren = {result} - + } else if (toolMessage.type === 'running_now') { const { result } = toolMessage @@ -2178,7 +2159,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'rewrite_file': { + 'rewrite_file': { resultWrapper: (params) => { return } @@ -2202,7 +2183,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'open_persistent_terminal': { + 'open_persistent_terminal': { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const terminalToolsService = accessor.get('ITerminalToolService') @@ -2214,7 +2195,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } @@ -2228,11 +2209,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else if (toolMessage.type === 'tool_error') { const { result } = toolMessage - componentParams.children = + componentParams.bottomChildren = {result} - + } return @@ -2251,7 +2232,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, if (toolMessage.type === 'tool_request') return null // do not show past requests if (toolMessage.type === 'running_now') return null // do not show running - const isError = toolMessage.type === 'tool_error' + const isError = false const isRejected = toolMessage.type === 'rejected' const { rawParams, params } = toolMessage const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, } @@ -2263,11 +2244,11 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } else if (toolMessage.type === 'tool_error') { const { result } = toolMessage - componentParams.children = + componentParams.bottomChildren = {result} - + } return @@ -2722,7 +2703,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => const desc1 = {uriDone ? getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown') - : `Running`} + : `Generating`} @@ -2795,7 +2776,7 @@ export const SidebarChat = () => { const sidebarRef = useRef(null) const scrollContainerRef = useRef(null) - const onSubmit = useCallback(async (_forceSubmit?: string) => { + const onSubmit = useCallback(async (_forceSubmit?: string) => { if (isDisabled && !_forceSubmit) return if (isRunning) return @@ -2817,7 +2798,7 @@ export const SidebarChat = () => { }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState]) - const onAbort = async () => { + const onAbort = async () => { const threadId = currentThread.id await chatThreadsService.abortRunning(threadId) } @@ -2941,7 +2922,7 @@ export const SidebarChat = () => { isStreaming={!!isRunning} isDisabled={isDisabled} showSelections={true} - showProspectiveSelections={previousMessagesHTML.length === 0} + // showProspectiveSelections={previousMessagesHTML.length === 0} selections={selections} setSelections={setSelections} onClickAnywhere={() => { textAreaRef.current?.focus() }} @@ -2981,8 +2962,6 @@ export const SidebarChat = () => {
- console.log('!!!', Object.keys(chatThreadsState.allThreads).length) - const threadPageInput =
diff --git a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx index 9a01792f..f4bc418f 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/util/inputs.tsx @@ -17,7 +17,7 @@ import { asCssVariable } from '../../../../../../../platform/theme/common/colorU import { inputBackground, inputForeground } from '../../../../../../../platform/theme/common/colorRegistry.js'; import { useFloating, autoUpdate, offset, flip, shift, size, autoPlacement } from '@floating-ui/react'; import { URI } from '../../../../../../../base/common/uri.js'; -import { getBasename } from '../sidebar-tsx/SidebarChat.js'; +import { getBasename, getFolderName } from '../sidebar-tsx/SidebarChat.js'; import { ChevronRight, File, Folder, FolderClosed, LucideProps } from 'lucide-react'; import { StagingSelectionItem } from '../../../../common/chatThreadServiceTypes.js'; @@ -55,12 +55,13 @@ export const WidgetComponent = ({ ctor, prop type GenerateNextOptions = (optionText: string) => Promise type Option = { - nameInMenu: string, + fullName: string, + abbreviatedName: string, iconInMenu: ForwardRefExoticComponent & RefAttributes>, // type for lucide-react components } & ( - | { nextOptions: Option[], generateNextOptions?: undefined, nameToPaste?: undefined } - | { nextOptions?: undefined, generateNextOptions: GenerateNextOptions, nameToPaste?: undefined } - | { leafNodeType: 'File' | 'Folder', nameToPaste: string, uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, } + | { leafNodeType?: undefined, nextOptions: Option[], generateNextOptions?: undefined, } + | { leafNodeType?: undefined, nextOptions?: undefined, generateNextOptions: GenerateNextOptions, } + | { leafNodeType: 'File' | 'Folder', uri: URI, nextOptions?: undefined, generateNextOptions?: undefined, } ) @@ -173,6 +174,13 @@ export function getRelativeWorkspacePath(accessor: ReturnType { + return getBasename(relativePath, 1) +} + const getOptionsAtPath = async (accessor: ReturnType, path: string[], optionText: string): Promise => { const toolsService = accessor.get('IToolsService') @@ -193,8 +201,8 @@ const getOptionsAtPath = async (accessor: ReturnType, path: leafNodeType: 'File', uri: uri, iconInMenu: File, - nameInMenu: relativePath, - nameToPaste: getBasename(relativePath, 2), + fullName: relativePath, + abbreviatedName: getAbbreviatedName(relativePath), } }) return res @@ -258,8 +266,8 @@ const getOptionsAtPath = async (accessor: ReturnType, path: leafNodeType: 'Folder', uri: uri, iconInMenu: Folder, // Folder - nameInMenu: relativePath, - nameToPaste: getBasename(relativePath, 2) + fullName: relativePath, + abbreviatedName: getAbbreviatedName(relativePath), })) satisfies Option[]; } } catch (error) { @@ -271,13 +279,15 @@ const getOptionsAtPath = async (accessor: ReturnType, path: const allOptions: Option[] = [ { - nameInMenu: 'files', + fullName: 'files', + abbreviatedName: 'files', iconInMenu: File, generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'files')) || [], }, { - nameInMenu: 'folders', - iconInMenu: FolderClosed, + fullName: 'folders', + abbreviatedName: 'folders', + iconInMenu: Folder, generateNextOptions: async (t) => (await searchForFilesOrFolders(t, 'folders')) || [], }, ] @@ -289,7 +299,7 @@ const getOptionsAtPath = async (accessor: ReturnType, path: for (const pn of path) { - const selectedOption = nextOptionsAtPath.find(o => o.nameInMenu.toLowerCase() === pn.toLowerCase()) + const selectedOption = nextOptionsAtPath.find(o => o.fullName.toLowerCase() === pn.toLowerCase()) if (!selectedOption) return []; @@ -304,10 +314,10 @@ const getOptionsAtPath = async (accessor: ReturnType, path: } const optionsAtPath = nextOptionsAtPath - .filter(o => isSubsequence(o.nameInMenu, optionText)) + .filter(o => isSubsequence(o.fullName, optionText)) .sort((a, b) => { // this is a hack but good for now - const scoreA = scoreSubsequence(a.nameInMenu, optionText); - const scoreB = scoreSubsequence(b.nameInMenu, optionText); + const scoreA = scoreSubsequence(a.fullName, optionText); + const scoreB = scoreSubsequence(b.fullName, optionText); return scoreB - scoreA; }) .slice(0, numOptionsToShow) // should go last because sorting/filtering should happen on all datapoints @@ -354,6 +364,12 @@ export const VoidInputBox2 = forwardRef(fun const [optionIdx, setOptionIdx] = useState(0); const [options, setOptions] = useState([]); const [optionText, setOptionText] = useState(''); + const [didLoadInitialOptions, setDidLoadInitialOptions] = useState(false); + + const currentPathRef = useRef(JSON.stringify([])); + const areBreadcrumbsShowing = didLoadInitialOptions && optionPath.length >= 1; + + const insertTextAtCursor = (text: string) => { const textarea = textAreaRef.current; if (!textarea) return; @@ -379,15 +395,12 @@ export const VoidInputBox2 = forwardRef(fun if (!options.length) { return; } const option = options[optionIdx]; - const newPath = [...optionPath, option.nameInMenu] + const newPath = [...optionPath, option.fullName] const isLastOption = !option.generateNextOptions && !option.nextOptions - - setOptionPath(newPath) - setOptionText('') - setOptionIdx(0) + setDidLoadInitialOptions(false) if (isLastOption) { setIsMenuOpen(false) - insertTextAtCursor(option.nameToPaste) + insertTextAtCursor(option.abbreviatedName) const newSelection: StagingSelectionItem = option.leafNodeType === 'File' ? { type: 'File', @@ -404,26 +417,39 @@ export const VoidInputBox2 = forwardRef(fun console.log('selected', option.uri?.fsPath) } else { + + + currentPathRef.current = JSON.stringify(newPath); const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] + if (currentPathRef.current !== JSON.stringify(newPath)) { return; } + setOptionPath(newPath) + setOptionText('') + setOptionIdx(0) setOptions(newOpts) + setDidLoadInitialOptions(true) } } const onRemoveOption = async () => { const newPath = [...optionPath.slice(0, optionPath.length - 1)] + currentPathRef.current = JSON.stringify(newPath); + const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] + if (currentPathRef.current !== JSON.stringify(newPath)) { return; } setOptionPath(newPath) setOptionText('') setOptionIdx(0) - const newOpts = await getOptionsAtPath(accessor, newPath, '') || [] setOptions(newOpts) } const onOpenOptionMenu = async () => { - setOptionPath([]) + const newPath: [] = [] + currentPathRef.current = JSON.stringify([]); + const newOpts = await getOptionsAtPath(accessor, [], '') || [] + if (currentPathRef.current !== JSON.stringify([])) { return; } + setOptionPath(newPath) setOptionText('') setIsMenuOpen(true); setOptionIdx(0); - const newOpts = await getOptionsAtPath(accessor, [], '') || [] setOptions(newOpts); } const onCloseOptionMenu = () => { @@ -469,15 +495,19 @@ export const VoidInputBox2 = forwardRef(fun // debounced const onPathTextChange = useCallback((newStr: string) => { + setOptionText(newStr); if (debounceTimerRef.current !== null) { window.clearTimeout(debounceTimerRef.current); } + currentPathRef.current = JSON.stringify(optionPath); + // Set a new timeout to fetch options after a delay debounceTimerRef.current = window.setTimeout(async () => { const newOpts = await getOptionsAtPath(accessor, optionPath, newStr) || []; + if (currentPathRef.current !== JSON.stringify(optionPath)) { return; } setOptions(newOpts); setOptionIdx(0); debounceTimerRef.current = null; @@ -537,7 +567,9 @@ export const VoidInputBox2 = forwardRef(fun // do nothing } else { // letter - onPathTextChange(optionText + e.key) + if (areBreadcrumbsShowing) { + onPathTextChange(optionText + e.key) + } } } @@ -715,6 +747,16 @@ export const VoidInputBox2 = forwardRef(fun return; } + if (e.key === 'Backspace') { // TODO allow user to undo this. + if (!e.currentTarget.value) { // if there is no text, remove a selection + if (e.metaKey || e.ctrlKey) { // Ctrl+Backspace = remove all + chatThreadService.popStagingSelections(Number.MAX_SAFE_INTEGER) + } else { // Backspace = pop 1 selection + chatThreadService.popStagingSelections(1) + } + return; + } + } if (e.key === 'Enter') { // Shift + Enter when multiline = newline const shouldAddNewline = e.shiftKey && multiline @@ -740,25 +782,25 @@ export const VoidInputBox2 = forwardRef(fun onWheel={(e) => e.stopPropagation()} > {/* Breadcrumbs Header */} -
- {optionPath.length || optionText ? + {areBreadcrumbsShowing &&
+ {optionText ?
- {optionPath.map((path, index) => ( + {/* {optionPath.map((path, index) => ( {path} - ))} + ))} */} {optionText}
- :
Enter text to filter...
+ :
Enter text to filter...
} -
+
} {/* Options list */}
-
+
{options.length === 0 ?
No results found
: options.map((o, oIdx) => { @@ -767,17 +809,19 @@ export const VoidInputBox2 = forwardRef(fun // Option
{ onSelectOption(); }} - onMouseOver={() => { setOptionIdx(oIdx) }} + onMouseMove={() => { setOptionIdx(oIdx) }} > {} - {o.nameInMenu} + {o.abbreviatedName} + + {o.fullName && o.fullName !== o.abbreviatedName && {o.fullName}} {o.nextOptions || o.generateNextOptions ? ( ) : null} diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx index 7e07184f..de913465 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-settings-tsx/Settings.tsx @@ -1053,7 +1053,7 @@ export const Settings = () => {

{displayInfoOfFeatureName('Apply')}

-
Settings that control the behavior of the Apply button and the Edit tool.
+
Settings that control the behavior of the Apply button.
{/* Sync to Chat Switch */} @@ -1126,7 +1126,7 @@ export const Settings = () => {

Editor

-
{`Settings that control the visibility of suggestions and widgets in the code editor.`}
+
{`Settings that control the visibility of Void suggestions in the code editor.`}
{/* Auto Accept Switch */} @@ -1162,10 +1162,9 @@ export const Settings = () => { {/* Import/Export section, as its own block right after One-Click Switch */}

Import/Export

-
+
{/* Settings Subcategory */}
-

Settings

{ fileInputSettingsRef.current?.click() }}> Import Settings @@ -1179,7 +1178,6 @@ export const Settings = () => {
{/* Chats Subcategory */}
-

Chat

{ fileInputChatsRef.current?.click() }}> Import Chats diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 188bb658..8566db21 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -295,12 +295,14 @@ export class ToolsService implements IToolsService { contents = model.getValueInRange({ startLineNumber, startColumn: 1, endLineNumber, endColumn: Number.MAX_SAFE_INTEGER }, EndOfLinePreference.LF) } + const totalNumLines = model.getLineCount() + const fromIdx = MAX_FILE_CHARS_PAGE * (pageNumber - 1) const toIdx = MAX_FILE_CHARS_PAGE * pageNumber - 1 const fileContents = contents.slice(fromIdx, toIdx + 1) // paginate const hasNextPage = (contents.length - 1) - toIdx >= 1 const totalFileLen = contents.length - return { result: { fileContents, totalFileLen, hasNextPage } } + return { result: { fileContents, totalFileLen, hasNextPage, totalNumLines } } }, ls_dir: async ({ uri, pageNumber }) => { @@ -414,7 +416,6 @@ export class ToolsService implements IToolsService { if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) } - console.log('aaaa', searchReplaceBlocks) editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }) // at end, get lint errors @@ -460,7 +461,7 @@ export class ToolsService implements IToolsService { // given to the LLM after the call for successful tool calls this.stringOfResult = { read_file: (params, result) => { - return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}` + return `${params.uri.fsPath}\n\`\`\`\n${result.fileContents}\n\`\`\`${nextPageStr(result.hasNextPage)}${result.hasNextPage ? `\nMore info because truncated: this file has ${result.totalNumLines} lines, or ${result.totalFileLen} characters.` : ''}` }, ls_dir: (params, result) => { const dirTreeStr = stringifyDirectoryTree1Deep(params, result) @@ -522,7 +523,7 @@ export class ToolsService implements IToolsService { } // normal command if (resolveReason.type === 'timeout') { - return `${result_}\nTerminal command ran, but was interrupted by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not necessarily finish successfully. To try with more time, open a persistent terminal and run the command there.` + return `${result_}\nTerminal command ran, but was automatically killed by Void after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity and did not finish successfully. To try with more time, open a persistent terminal and run the command there.` } throw new Error(`Unexpected internal error: Terminal command did not resolve with a valid reason.`) }, diff --git a/src/vs/workbench/contrib/void/common/helpers/colors.ts b/src/vs/workbench/contrib/void/common/helpers/colors.ts index 1a20ea9b..17978f76 100644 --- a/src/vs/workbench/contrib/void/common/helpers/colors.ts +++ b/src/vs/workbench/contrib/void/common/helpers/colors.ts @@ -1,8 +1,40 @@ -export const acceptBg = '#1a7431' -export const acceptAllBg = '#1e8538' -export const acceptBorder = '1px solid #145626' -export const rejectBg = '#b42331' -export const rejectAllBg = '#cf2838' -export const rejectBorder = '1px solid #8e1c27' +/*-------------------------------------------------------------------------------------- + * Copyright 2025 Glass Devtools, Inc. All rights reserved. + * Licensed under the Apache License, Version 2.0. See LICENSE.txt for more information. + *--------------------------------------------------------------------------------------*/ + +import { Color, RGBA } from '../../../../../base/common/color.js'; +import { registerColor } from '../../../../../platform/theme/common/colorUtils.js'; + +// editCodeService colors +const sweepBG = new Color(new RGBA(100, 100, 100, .2)); +const highlightBG = new Color(new RGBA(100, 100, 100, .1)); +const sweepIdxBG = new Color(new RGBA(100, 100, 100, .5)); + +const acceptBG = new Color(new RGBA(155, 185, 85, .1)); // default is RGBA(155, 185, 85, .2) +const rejectBG = new Color(new RGBA(255, 0, 0, .1)); // default is RGBA(255, 0, 0, .2) + +// Widget colors +export const acceptAllBg = 'rgb(30, 133, 56)' +export const acceptBg = 'rgb(26, 116, 48)' +export const acceptBorder = '1px solid rgb(20, 86, 38)' + +export const rejectAllBg = 'rgb(207, 40, 56)' +export const rejectBg = 'rgb(180, 35, 49)' +export const rejectBorder = '1px solid rgb(142, 28, 39)' + export const buttonFontSize = '11px' export const buttonTextColor = 'white' + + + +const configOfBG = (color: Color) => { + return { dark: color, light: color, hcDark: color, hcLight: color, } +} + +// gets converted to --vscode-void-greenBG, see void.css, asCssVariable +registerColor('void.greenBG', configOfBG(acceptBG), '', true); +registerColor('void.redBG', configOfBG(rejectBG), '', true); +registerColor('void.sweepBG', configOfBG(sweepBG), '', true); +registerColor('void.highlightBG', configOfBG(highlightBG), '', true); +registerColor('void.sweepIdxBG', configOfBG(sweepIdxBG), '', true); diff --git a/src/vs/workbench/contrib/void/common/modelCapabilities.ts b/src/vs/workbench/contrib/void/common/modelCapabilities.ts index 7fad5d59..79039f7b 100644 --- a/src/vs/workbench/contrib/void/common/modelCapabilities.ts +++ b/src/vs/workbench/contrib/void/common/modelCapabilities.ts @@ -107,8 +107,8 @@ export const defaultModelsOfProvider = { 'anthropic/claude-3.5-sonnet', 'deepseek/deepseek-r1', 'deepseek/deepseek-r1-zero:free', - 'openrouter/quasar-alpha', - 'google/gemini-2.5-pro-preview-03-25', + // 'openrouter/quasar-alpha', + // 'google/gemini-2.5-pro-preview-03-25', // 'mistralai/codestral-2501', // 'qwen/qwen-2.5-coder-32b-instruct', // 'mistralai/mistral-small-3.1-24b-instruct:free', @@ -153,7 +153,7 @@ export type VoidStaticModelInfo = { // not stateful } supportsSystemMessage: false | 'system-role' | 'developer-role' | 'separated'; // separated = anthropic where "system" is a special paramete - specialToolFormat?: 'openai-style' | 'anthropic-style', // null defaults to XML + specialToolFormat?: 'openai-style' | 'anthropic-style' | 'gemini-style', // null defaults to XML supportsFIM: boolean; reasoningCapabilities: false | { @@ -298,6 +298,12 @@ const openSourceModelOptions_assumingOAICompat = { reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: false, canIOReasoning: true, openSourceThinkTags: ['', ''] }, contextWindow: 128_000, maxOutputTokens: 8_192, }, + 'qwen3': { + supportsFIM: false, // replaces QwQ + supportsSystemMessage: 'system-role', + reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: true, canIOReasoning: true, openSourceThinkTags: ['', ''] }, + contextWindow: 32_768, maxOutputTokens: 8_192, + }, // FIM only 'starcoder2': { supportsFIM: true, @@ -359,6 +365,8 @@ const extensiveModelFallback: VoidStaticProviderInfo['modelOptionsFallback'] = ( if (lower.includes('llama')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['llama4-scout'] }) if (lower.includes('qwen') && lower.includes('2.5') && lower.includes('coder')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen2.5coder'] }) + if (lower.includes('qwen') && lower.includes('3')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen3'] }) + if (lower.includes('qwen')) return toFallback({ ...openSourceModelOptions_assumingOAICompat['qwen3'] }) if (lower.includes('qwq')) { return toFallback({ ...openSourceModelOptions_assumingOAICompat.qwq, }) } if (lower.includes('phi4')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.phi4, }) if (lower.includes('codestral')) return toFallback({ ...openSourceModelOptions_assumingOAICompat.codestral }) @@ -639,7 +647,8 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing cost: { input: 0.15, output: .60 }, // TODO $3.50 output with thinking not included downloadable: false, supportsFIM: false, - supportsSystemMessage: 'system-role', + supportsSystemMessage: 'separated', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-2.5-pro-exp-03-25': { @@ -648,7 +657,8 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing cost: { input: 0, output: 0 }, downloadable: false, supportsFIM: false, - supportsSystemMessage: 'system-role', + supportsSystemMessage: 'separated', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-2.0-flash': { @@ -657,7 +667,8 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing cost: { input: 0.10, output: 0.40 }, downloadable: false, supportsFIM: false, - supportsSystemMessage: 'system-role', + supportsSystemMessage: 'separated', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-2.0-flash-lite-preview-02-05': { @@ -666,7 +677,8 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing cost: { input: 0.075, output: 0.30 }, downloadable: false, supportsFIM: false, - supportsSystemMessage: 'system-role', + supportsSystemMessage: 'separated', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-1.5-flash': { @@ -675,7 +687,8 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing cost: { input: 0.075, output: 0.30 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now downloadable: false, supportsFIM: false, - supportsSystemMessage: 'system-role', + supportsSystemMessage: 'separated', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-1.5-pro': { @@ -684,7 +697,8 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing cost: { input: 1.25, output: 5.00 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now downloadable: false, supportsFIM: false, - supportsSystemMessage: 'system-role', + supportsSystemMessage: 'separated', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, 'gemini-1.5-flash-8b': { @@ -693,7 +707,8 @@ const geminiModelOptions = { // https://ai.google.dev/gemini-api/docs/pricing cost: { input: 0.0375, output: 0.15 }, // TODO!!! price doubles after 128K tokens, we are NOT encoding that info right now downloadable: false, supportsFIM: false, - supportsSystemMessage: 'system-role', + supportsSystemMessage: 'separated', + specialToolFormat: 'gemini-style', reasoningCapabilities: false, }, } as const satisfies { [s: string]: VoidStaticModelInfo } diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index f08f41bc..d0e27fcd 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -124,23 +124,18 @@ ${searchReplaceBlockTemplate} // ======================================================== tools ======================================================== -const changesExampleContent = `\ + + +const chatSuggestionDiffExample = `\ +${tripleTick[0]}typescript +/Users/username/Dekstop/my_project/app.ts // ... existing code ... // {{change 1}} // ... existing code ... // {{change 2}} // ... existing code ... // {{change 3}} -// ... existing code ...` - -const editToolDescriptionExample = `\ -${tripleTick[0]} -${changesExampleContent} -${tripleTick[1]}` - -const chatSuggestionDiffExample = `${tripleTick[0]}typescript -/Users/username/Dekstop/my_project/app.ts -${changesExampleContent} +// ... existing code ... ${tripleTick[1]}` @@ -185,15 +180,6 @@ export type SnakeCaseKeys> = { -const applyToolDescription = (type: 'edit tool' | 'chat suggestion') => `\ -${type === 'edit tool' ? 'A' : 'a'} code diff describing the change to make to the file. \ -Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \ -Your DIFF MUST be wrapped in triple backticks. \ -NEVER re-write the whole file. Always bias towards writing as little as possible. \ -Use comments like "// ... existing code ..." to condense your writing. \ -Here's an example of a good output:\n${type === 'edit tool' ? editToolDescriptionExample : chatSuggestionDiffExample}` - - // export const voidTools = { export const voidTools : { @@ -212,8 +198,8 @@ export const voidTools description: `Returns full contents of a given file.`, params: { ...uriParam('file'), - start_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to 1.' }, - end_line: { description: 'Optional. Do NOT fill this in unless you already know the line numbers you need to search. Defaults to Infinity.' }, + start_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the beginning of the file.' }, + end_line: { description: 'Optional. Do NOT fill this field in unless you were specifically given exact line numbers to search. Defaults to the end of the file.' }, ...paginationParam, }, }, @@ -275,7 +261,7 @@ export const voidTools read_lint_errors: { name: 'read_lint_errors', - description: `Returns all lint errors on a given file.`, + description: `Use this tool to view all the lint errors on a file.`, params: { ...uriParam('file'), }, @@ -499,11 +485,13 @@ ${directoryStr} - The remaining contents of the file should proceed as usual.`) details.push(`If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S). -- The first line of the code block must be the FULL PATH of the related file. -- The remaining contents should be ${applyToolDescription('chat suggestion')}`) +- The first line of the code block must be the FULL PATH of the related file if known (otherwise omit). +- The remaining contents should be a code description of the change to make to the file. \ +Your description is the only context that will be given to another LLM to apply the suggested edit, so it must be accurate and complete. \ +Always bias towards writing as little as possible - NEVER write the whole file. Use comments like "// ... existing code ..." to condense your writing. \ +Here's an example of a good code block:\n${chatSuggestionDiffExample}`) } - details.push(`NEVER write the FULL PATH of a file when speaking with the user. Just write the file name ONLY.`) details.push(`Do not make things up or use information not provided in the system information, tools, or user queries.`) details.push(`Always use MARKDOWN to format lists, bullet points, etc. Do NOT write tables.`) details.push(`Today's date is ${new Date().toDateString()}.`) diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index 6f08096f..d76d6685 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -51,8 +51,22 @@ export type OpenAILLMChatMessage = { content: string; tool_call_id: string; } -export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage +export type GeminiLLMChatMessage = { + role: 'model' + parts: ( + | { text: string; } + | { functionCall: { name: ToolName, args: object } } + )[]; +} | { + role: 'user'; + parts: ( + | { text: string; } + | { functionResponse: { name: ToolName, response: { result: string } } } + )[]; +} + +export type LLMChatMessage = AnthropicLLMChatMessage | OpenAILLMChatMessage | GeminiLLMChatMessage diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index b6c466f4..d97372fa 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -57,7 +57,7 @@ export type ToolCallParams = { // RESULT OF TOOL CALL export type ToolResultType = { - 'read_file': { fileContents: string, totalFileLen: number, hasNextPage: boolean }, + 'read_file': { fileContents: string, totalFileLen: number, totalNumLines: number, hasNextPage: boolean }, 'ls_dir': { children: ShallowDirectoryItem[] | null, hasNextPage: boolean, hasPrevPage: boolean, itemsRemaining: number }, 'get_dir_tree': { str: string, }, 'search_pathnames_only': { uris: URI[], hasNextPage: boolean }, 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 4a515e50..6eb0ad7a 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 @@ -10,6 +10,7 @@ import { Ollama } from 'ollama'; import OpenAI, { ClientOptions } from 'openai'; import { MistralCore } from '@mistralai/mistralai/core.js'; import { fimComplete } from '@mistralai/mistralai/funcs/fimComplete.js'; +import { GoogleGenerativeAI, Tool as GeminiTool, SchemaType, FunctionDeclaration, FunctionDeclarationSchemaProperty } from '@google/generative-ai'; // import { GoogleAuth } from 'google-auth-library' /* eslint-enable */ @@ -18,6 +19,7 @@ import { ChatMode, displayInfoOfProviderName, ModelSelectionOptions, ProviderNam import { getSendableReasoningInfo, getModelCapabilities, getProviderCapabilities, defaultProviderSettings, getMaxOutputTokens } from '../../common/modelCapabilities.js'; import { extractReasoningWrapper, extractXMLToolsWrapper } from './extractGrammar.js'; import { availableTools, InternalToolInfo, isAToolName, ToolParamName, voidTools } from '../../common/prompt/prompts.js'; +import { generateUuid } from '../../../../../base/common/uuid.js'; @@ -33,7 +35,11 @@ type InternalCommonMessageParams = { _setAborter: (aborter: () => void) => void; } -type SendChatParams_Internal = InternalCommonMessageParams & { messages: LLMChatMessage[]; separateSystemMessage: string | undefined; chatMode: ChatMode | null; } +type SendChatParams_Internal = InternalCommonMessageParams & { + messages: LLMChatMessage[]; + separateSystemMessage: string | undefined; + chatMode: ChatMode | null; +} type SendFIMParams_Internal = InternalCommonMessageParams & { messages: LLMFIMMessage; separateSystemMessage: string | undefined; } export type ListParams_Internal = ModelListParams @@ -42,13 +48,6 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI // ------------ OPENAI-COMPATIBLE (HELPERS) ------------ -// const getGoogleApiKey = async () => { -// // module‑level singleton -// const auth = new GoogleAuth({ scopes: `https://www.googleapis.com/auth/cloud-platform` }); -// const key = await auth.getAccessToken() -// if (!key) throw new Error(`Google API failed to generate a key.`) -// return key -// } const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includeInPayload }: { settingsOfProvider: SettingsOfProvider, providerName: ProviderName, includeInPayload?: { [s: string]: any } }) => { @@ -88,10 +87,6 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ ...commonPayloadOpts, }) } - else if (providerName === 'gemini') { - const thisConfig = settingsOfProvider[providerName] - return new OpenAI({ baseURL: 'https://generativelanguage.googleapis.com/v1beta/openai', apiKey: thisConfig.apiKey, ...commonPayloadOpts }) - } // else if (providerName === 'googleVertex') { // // https://cloud.google.com/vertex-ai/generative-ai/docs/multimodal/call-vertex-using-openai-library // const thisConfig = settingsOfProvider[providerName] @@ -131,6 +126,7 @@ const newOpenAICompatibleSDK = async ({ settingsOfProvider, providerName, includ const _sendOpenAICompatibleFIM = async ({ messages: { prefix, suffix, stopTokens }, onFinalMessage, onError, settingsOfProvider, modelName: modelName_, _setAborter, providerName, }: SendFIMParams_Internal) => { + const { modelName, supportsFIM } = getModelCapabilities(providerName, modelName_) if (!supportsFIM) { if (modelName === modelName_) @@ -193,7 +189,7 @@ const openAITools = (chatMode: ChatMode) => { return openAITools } -const openAIToolToRawToolCallObj = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { +const rawToolCallObjOf = (name: string, toolParamsStr: string, id: string): RawToolCallObj | null => { if (!isAToolName(name)) return null const rawParams: RawToolParamsObj = {} let input: unknown @@ -297,6 +293,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE fullReasoningSoFar += newReasoning } + // call onText onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, @@ -309,7 +306,7 @@ const _sendOpenAICompatibleChat = async ({ messages, onText, onFinalMessage, onE onError({ message: 'Void: Response from model was empty.', fullError: null }) } else { - const toolCall = openAIToolToRawToolCallObj(toolName, toolParamsStr, toolId) + const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) const toolCallObj = toolCall ? { toolCall } : {} onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); } @@ -438,7 +435,7 @@ const sendAnthropicChat = async ({ messages, providerName, onText, onFinalMessag }) - // manually parse out tool results + // manually parse out tool results if XML if (!specialToolFormat) { const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) onText = newOnText @@ -544,6 +541,7 @@ const sendMistralFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, stop: messages.stopTokens, }) .then(async response => { + // unfortunately, _setAborter() does not exist let content = response?.ok ? response.value.choices?.[0]?.message?.content ?? '' : ''; const fullText = typeof content === 'string' ? content @@ -620,6 +618,163 @@ const sendOllamaFIM = ({ messages, onFinalMessage, onError, settingsOfProvider, }) } +// ---------------- GEMINI NATIVE IMPLEMENTATION ---------------- + + + +const toGeminiFunctionDecl = (toolInfo: InternalToolInfo) => { + const { name, description, params } = toolInfo + const paramsWithType: { [k: string]: FunctionDeclarationSchemaProperty } = {} + for (const key in params) { + paramsWithType[key] = { type: SchemaType.STRING, ...params[key] } + } + return { + name, + description, + parameters: { + type: SchemaType.OBJECT, + properties: paramsWithType, + } + } satisfies FunctionDeclaration +} + + +const geminiTools = (chatMode: ChatMode): GeminiTool[] | null => { + const allowedTools = availableTools(chatMode) + if (!allowedTools || Object.keys(allowedTools).length === 0) return null + const functionDecls: FunctionDeclaration[] = [] + for (const t in allowedTools ?? {}) { + functionDecls.push(toGeminiFunctionDecl(allowedTools[t])) + } + const tools: GeminiTool = { functionDeclarations: functionDecls, } + return [tools] +} + + + +// Implementation for Gemini using Google's native API +const sendGeminiChat = async ({ + messages, + separateSystemMessage, + onText, + onFinalMessage, + onError, + settingsOfProvider, + modelName: modelName_, + _setAborter, + providerName, + modelSelectionOptions, + chatMode, +}: SendChatParams_Internal) => { + + if (providerName !== 'gemini') throw new Error(`Sending Gemini chat, but provider was ${providerName}`) + + const thisConfig = settingsOfProvider[providerName] + + const { + modelName, + specialToolFormat, + // reasoningCapabilities, + } = getModelCapabilities(providerName, modelName_) + + const { providerReasoningIOSettings } = getProviderCapabilities(providerName) + + // reasoning + // const { canIOReasoning, openSourceThinkTags, } = reasoningCapabilities || {} + const reasoningInfo = getSendableReasoningInfo('Chat', providerName, modelName_, modelSelectionOptions) // user's modelName_ here + const includeInPayload = providerReasoningIOSettings?.input?.includeInPayload?.(reasoningInfo) || {} + + // tools + const potentialTools = chatMode !== null ? geminiTools(chatMode) : null + const nativeToolsObj = potentialTools && specialToolFormat === 'gemini-style' ? + { tools: potentialTools } as const + : {} + + // instance + const genAI = new GoogleGenerativeAI( + thisConfig.apiKey + ); + const model = genAI.getGenerativeModel({ + systemInstruction: separateSystemMessage, + model: modelName, + }); + + // manually parse out tool results if XML + if (!specialToolFormat) { + const { newOnText, newOnFinalMessage } = extractXMLToolsWrapper(onText, onFinalMessage, chatMode) + onText = newOnText + onFinalMessage = newOnFinalMessage + } + + // when receive text + let fullReasoningSoFar = '' + let fullTextSoFar = '' + + let toolName = '' + let toolParamsStr = '' + + model.generateContentStream({ + systemInstruction: separateSystemMessage ?? undefined, + contents: messages as any, + ...includeInPayload, + ...nativeToolsObj, + }) + .then(async ({ stream, response }) => { + _setAborter(() => { stream.return(fullTextSoFar); }); + + // Process the stream + for await (const chunk of stream) { + // message + const newText = chunk.text() ?? '' + fullTextSoFar += newText + + // tool call + const functionCalls = chunk.functionCalls() + if (functionCalls && functionCalls.length > 0) { + const functionCall = functionCalls[0] // Get the first function call + toolName = functionCall.name ?? '' + toolParamsStr = JSON.stringify(functionCall.args ?? {}) + } + + // (do not handle reasoning yet) + + // call onText + onText({ + fullText: fullTextSoFar, + fullReasoning: fullReasoningSoFar, + toolCall: isAToolName(toolName) ? { name: toolName, rawParams: {}, isDone: false, doneParams: [], id: 'dummy' } : undefined, + }) + } + + // on final + if (!fullTextSoFar && !fullReasoningSoFar && !toolName) { + onError({ message: 'Void: Response from model was empty.', fullError: null }) + } else { + const toolId = generateUuid() // gemini does not generate tool IDs. Generate one + const toolCall = rawToolCallObjOf(toolName, toolParamsStr, toolId) + const toolCallObj = toolCall ? { toolCall } : {} + onFinalMessage({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, anthropicReasoning: null, ...toolCallObj }); + } + }) + .catch(error => { + const message = error?.message + if (typeof message === 'string') { + + if (error.message?.includes('API key')) { + onError({ message: invalidApiKeyMessage(providerName), fullError: error }); + } + else if (error?.message?.includes('429')) { + onError({ message: 'Rate limit reached. ' + error, fullError: error }); + } + else + onError({ message: error + '', fullError: error }); + } + else { + onError({ message: error + '', fullError: error }); + } + }) +}; + type CallFnOfProvider = { @@ -647,7 +802,7 @@ export const sendLLMMessageToProviderImplementation = { list: null, }, gemini: { - sendChat: (params) => _sendOpenAICompatibleChat(params), + sendChat: (params) => sendGeminiChat(params), sendFIM: null, list: null, }, 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 f19c9b1b..e1c18f50 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -33,13 +33,6 @@ export const sendLLMMessage = async ({ // only captures number of messages and message "shape", no actual code, instructions, prompts, etc const captureLLMEvent = (eventId: string, extras?: object) => { - let totalTokens = 0 - if (messagesType === 'chatMessages') { - for (const m of messages_) totalTokens += m.content.length - } - else { - totalTokens = messages_.prefix.length + messages_.suffix.length - } metricsService.capture(eventId, { providerName, @@ -48,13 +41,10 @@ export const sendLLMMessage = async ({ numModelsAtEndpoint: settingsOfProvider[providerName]?.models?.length, ...messagesType === 'chatMessages' ? { numMessages: messages_?.length, - messagesShape: messages_?.map(msg => ({ role: msg.role, length: msg.content.length })), - } : messagesType === 'FIMMessage' ? { prefixLength: messages_.prefix.length, suffixLength: messages_.suffix.length, } : {}, - totalTokens, ...loggingExtras, ...extras, }) @@ -103,7 +93,7 @@ export const sendLLMMessage = async ({ if (messagesType === 'chatMessages') - captureLLMEvent(`${loggingName} - Sending Message`, { userMessageLength: messages_?.[messages_.length - 1]?.content.length }) + captureLLMEvent(`${loggingName} - Sending Message`, {}) else if (messagesType === 'FIMMessage') captureLLMEvent(`${loggingName} - Sending FIM`, { prefixLen: messages_?.prefix?.length, suffixLen: messages_?.suffix?.length })