Merge pull request #439 from voideditor/model-selection

Edit fixes
This commit is contained in:
Andrew Pareles 2025-04-30 22:14:17 -07:00 committed by GitHub
commit c5bf5e453e
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
17 changed files with 742 additions and 431 deletions

View file

@ -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,

8
package-lock.json generated
View file

@ -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"

View file

@ -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",

View file

@ -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<UserMessageState>, messageIdx: number): void {

View file

@ -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<IConvertToLLMMessageService>('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 };
}

View file

@ -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

View file

@ -292,7 +292,7 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
className = '',
showModelDropdown = true,
showSelections = false,
showProspectiveSelections = true,
showProspectiveSelections = false,
selections,
setSelections,
featureName,
@ -314,11 +314,6 @@ export const VoidChatArea: React.FC<VoidChatAreaProps> = ({
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 (<div className=''>
<div className={`w-full border border-void-border-3 rounded px-2 py-1 bg-void-bg-3 overflow-hidden ${className}`}>
{/* header */}
<div className={`select-none flex items-center min-h-[24px] ${!isDropdown ? 'mx-1' : ''}`}>
<div className={`select-none flex items-center min-h-[24px]`}>
<div className={`flex items-center w-full gap-x-2 overflow-hidden justify-between ${isRejected ? 'line-through' : ''}`}>
{/* left */}
<div className={`
@ -810,7 +805,7 @@ const ToolHeaderWrapper = ({
const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<ResultWrapper<'edit_file' | 'rewrite_file'>>[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<Res
}
else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
// add apply box
if (params) {
const applyBoxId = getApplyBoxId({
threadId: threadId,
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={content}
/>
}
const applyBoxId = getApplyBoxId({
threadId: threadId,
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={content}
/>
// add children
if (toolMessage.type !== 'tool_error') {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
if (toolMessage.type === 'success' || toolMessage.type === 'rejected') {
const { result } = toolMessage
componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Lint errors'>
{result?.lintErrors?.map((error, i) => (
<div key={i} className='whitespace-nowrap'>Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
</BottomChildren>
}
else {
else if (toolMessage.type === 'tool_error') {
// error
const { result } = toolMessage
if (params) {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
{/* error */}
<CodeChildren>
{result}
</CodeChildren>
{/* content */}
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
}
else {
componentParams.children = <CodeChildren>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
}
</BottomChildren>
}
}
@ -1063,87 +1044,87 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isCheckpointGhost, curr
setSelections={setStagingSelections}
>
<VoidInputBox2
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}
/>
</VoidChatArea>
}
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}
/>
</VoidChatArea>
}
const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1
const isMsgAfterCheckpoint = currCheckpointIdx !== undefined && currCheckpointIdx === messageIdx - 1
return <div
// align chatbubble accoridng to role
className={`
return <div
// align chatbubble accoridng to role
className={`
relative ml-auto
${mode === 'edit' ? 'w-full max-w-full'
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
}
: mode === 'display' ? `self-end w-fit max-w-full whitespace-pre-wrap` : '' // user words should be pre
}
${isCheckpointGhost && !isMsgAfterCheckpoint ? 'opacity-50 pointer-events-none' : ''}
`}
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
// style chatbubble according to role
className={`
onMouseEnter={() => setIsHovered(true)}
onMouseLeave={() => setIsHovered(false)}
>
<div
// style chatbubble according to role
className={`
text-left rounded-lg max-w-full
${mode === 'edit' ? ''
: mode === 'display' ? 'p-2 flex flex-col bg-void-bg-1 text-void-fg-1 overflow-x-auto cursor-pointer' : ''
}
: mode === 'display' ? 'p-2 flex flex-col bg-void-bg-1 text-void-fg-1 overflow-x-auto cursor-pointer' : ''
}
`}
onClick={() => { if (mode === 'display') { onOpenEdit() } }}
>
{chatbubbleContents}
</div>
onClick={() => { if (mode === 'display') { onOpenEdit() } }}
>
{chatbubbleContents}
</div>
<div
className="absolute -top-1 -right-1 translate-x-0 -translate-y-0 z-1"
// data-tooltip-id='void-tooltip'
// data-tooltip-content='Edit message'
// data-tooltip-place='left'
>
<EditSymbol
size={18}
className={`
<div
className="absolute -top-1 -right-1 translate-x-0 -translate-y-0 z-1"
// data-tooltip-id='void-tooltip'
// data-tooltip-content='Edit message'
// data-tooltip-place='left'
>
<EditSymbol
size={18}
className={`
cursor-pointer
p-[2px]
bg-void-bg-1 border border-void-border-1 rounded-md
transition-opacity duration-200 ease-in-out
${isHovered || (isFocused && mode === 'edit') ? 'opacity-100' : 'opacity-0'}
`}
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>
</div>
onClick={() => {
if (mode === 'display') {
onOpenEdit()
} else if (mode === 'edit') {
onCloseEdit()
}
}}
/>
</div>
</div>
</div>
}
const SmallProseWrapper = ({ children }: { children: React.ReactNode }) => {
return <div className='
return <div className='
text-void-fg-4
prose
prose-sm
@ -1200,12 +1181,12 @@ prose-pre:my-2
prose-table:text-[13px]
'>
{children}
</div>
{children}
</div>
}
const ProseWrapper = ({ children }: { children: React.ReactNode }) => {
return <div className='
return <div className='
text-void-fg-2
prose
prose-sm
@ -1230,77 +1211,77 @@ prose-ul:leading-normal
max-w-none
'
>
{children}
</div>
>
{children}
</div>
}
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 &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
<SmallProseWrapper>
<ChatMarkdownRender
string={reasoningStr}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={false}
isLinkDetectionEnabled={true}
/>
</SmallProseWrapper>
</ReasoningWrapper>
</div>
}
return <>
{/* reasoning token */}
{hasReasoning &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ReasoningWrapper isDoneReasoning={isDoneReasoning} isStreaming={!isCommitted}>
<SmallProseWrapper>
<ChatMarkdownRender
string={reasoningStr}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={false}
isLinkDetectionEnabled={true}
/>
</SmallProseWrapper>
</ReasoningWrapper>
</div>
}
{/* assistant message */}
{chatMessage.displayContent &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ProseWrapper>
<ChatMarkdownRender
string={chatMessage.displayContent || ''}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={true}
isLinkDetectionEnabled={true}
/>
</ProseWrapper>
</div>
}
</>
{/* assistant message */}
{chatMessage.displayContent &&
<div className={`${isCheckpointGhost ? 'opacity-50' : ''}`}>
<ProseWrapper>
<ChatMarkdownRender
string={chatMessage.displayContent || ''}
chatMessageLocation={chatMessageLocation}
isApplyEnabled={true}
isLinkDetectionEnabled={true}
/>
</ProseWrapper>
</div>
}
</>
}
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 <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}>
<ToolChildrenWrapper>
<div className='!select-text cursor-auto'>
{children}
</div>
</ToolChildrenWrapper>
</ToolHeaderWrapper>
const isDone = isDoneReasoning || !isStreaming
const isWriting = !isDone
const [isOpen, setIsOpen] = useState(isWriting)
useEffect(() => {
if (!isWriting) setIsOpen(false) // if just finished reasoning, close
}, [isWriting])
return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}>
<ToolChildrenWrapper>
<div className='!select-text cursor-auto'>
{children}
</div>
</ToolChildrenWrapper>
</ToolHeaderWrapper>
}
@ -1309,10 +1290,10 @@ return <ToolHeaderWrapper title='Reasoning' desc1={isWriting ? <IconLoading /> :
// 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 <span className='flex items-center flex-nowrap'>
{item}
<IconLoading className='w-3 text-sm' />
</span>
return <span className='flex items-center flex-nowrap'>
{item}
<IconLoading className='w-3 text-sm' />
</span>
}
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[] }) => {
</div>
}
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 (
<div className="w-full px-2 mt-0.5">
@ -1590,15 +1571,13 @@ const EditToolLintErrors = ({ lintErrors }: { lintErrors: LintErrorItem[] }) =>
<ChevronRight
className={`mr-1 h-3 w-3 flex-shrink-0 transition-transform duration-100 text-void-fg-4 group-hover:text-void-fg-3 ${isOpen ? 'rotate-90' : ''}`}
/>
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">Lint errors</span>
<span className="font-medium text-void-fg-4 group-hover:text-void-fg-3 text-xs">{title}</span>
</div>
<div
className={`overflow-hidden transition-all duration-200 ease-in-out ${isOpen ? 'opacity-100' : 'max-h-0 opacity-0'} text-xs pl-4`}
>
<div className="flex flex-col gap-0.5 overflow-x-auto whitespace-nowrap text-void-fg-4 opacity-90 border-l-2 border-void-warning px-2 py-0.5">
{lintErrors.map((error, i) => (
<div key={i} className="">Lines {error.startLineNumber}-{error.endLineNumber}: {error.message}</div>
))}
<div className="overflow-x-auto text-void-fg-4 opacity-90 border-l-2 border-void-warning px-2 py-0.5">
{children}
</div>
</div>
</div>
@ -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 = <ToolChildrenWrapper>
{result}
</ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</BottomChildren>
}
else if (toolMessage.type === 'running_now') {
componentParams.children = <div ref={divRef} className='relative h-[300px] text-sm' />
@ -1760,7 +1741,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
@ -1805,7 +1786,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
}
else {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
@ -1853,7 +1834,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
}
else {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
@ -1899,7 +1880,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
}
else {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
@ -1948,7 +1929,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
}
else {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
}
@ -2004,7 +1985,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
</CodeChildren>
</ToolChildrenWrapper>
}
else {
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage;
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>;
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />;
@ -2060,7 +2041,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
if (params) componentParams.desc2 = <JumpToFileButton uri={params.uri} />
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
@ -2096,7 +2077,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
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 = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
else if (toolMessage.type === 'running_now') {
// nothing more is needed
@ -2139,7 +2120,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
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 = <ToolChildrenWrapper>
componentParams.children = componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
else if (toolMessage.type === 'running_now') {
const { result } = toolMessage
@ -2178,7 +2159,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <ToolHeaderWrapper {...componentParams} />
}
},
'rewrite_file': {
'rewrite_file': {
resultWrapper: (params) => {
return <EditTool {...params} content={`${'```\n'}${params.toolMessage.params.newContent}${'\n```'}`} />
}
@ -2202,7 +2183,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <CommandTool {...params} type='run_persistent_command' />
}
},
'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<T>,
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<T>,
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
@ -2251,7 +2232,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
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<T>,
}
else if (toolMessage.type === 'tool_error') {
const { result } = toolMessage
componentParams.children = <ToolChildrenWrapper>
componentParams.bottomChildren = <BottomChildren title='Error'>
<CodeChildren>
{result}
</CodeChildren>
</ToolChildrenWrapper>
</BottomChildren>
}
return <ToolHeaderWrapper {...componentParams} />
@ -2722,7 +2703,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
const desc1 = <span className='flex items-center'>
{uriDone ?
getBasename(toolCallSoFar.rawParams['uri'] ?? 'unknown')
: `Running`}
: `Generating`}
<IconLoading />
</span>
@ -2795,7 +2776,7 @@ export const SidebarChat = () => {
const sidebarRef = useRef<HTMLDivElement>(null)
const scrollContainerRef = useRef<HTMLDivElement | null>(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 = () => {
</div>
console.log('!!!', Object.keys(chatThreadsState.allThreads).length)
const threadPageInput = <div key={'input' + chatThreadsState.currentThreadId}>
<div className='px-4'>

View file

@ -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 = <CtorParams extends any[], Instance>({ ctor, prop
type GenerateNextOptions = (optionText: string) => Promise<Option[]>
type Option = {
nameInMenu: string,
fullName: string,
abbreviatedName: string,
iconInMenu: ForwardRefExoticComponent<Omit<LucideProps, "ref"> & RefAttributes<SVGSVGElement>>, // 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<typeof useAccessor
const numOptionsToShow = 100
// TODO make this unique based on other options
const getAbbreviatedName = (relativePath: string) => {
return getBasename(relativePath, 1)
}
const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, path: string[], optionText: string): Promise<Option[]> => {
const toolsService = accessor.get('IToolsService')
@ -193,8 +201,8 @@ const getOptionsAtPath = async (accessor: ReturnType<typeof useAccessor>, 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<typeof useAccessor>, 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<typeof useAccessor>, 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<typeof useAccessor>, 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<typeof useAccessor>, 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<HTMLTextAreaElement, InputBox2Props>(fun
const [optionIdx, setOptionIdx] = useState<number>(0);
const [options, setOptions] = useState<Option[]>([]);
const [optionText, setOptionText] = useState<string>('');
const [didLoadInitialOptions, setDidLoadInitialOptions] = useState(false);
const currentPathRef = useRef<string>(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<HTMLTextAreaElement, InputBox2Props>(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<HTMLTextAreaElement, InputBox2Props>(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<HTMLTextAreaElement, InputBox2Props>(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<HTMLTextAreaElement, InputBox2Props>(fun
// do nothing
}
else { // letter
onPathTextChange(optionText + e.key)
if (areBreadcrumbsShowing) {
onPathTextChange(optionText + e.key)
}
}
}
@ -715,6 +747,16 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(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<HTMLTextAreaElement, InputBox2Props>(fun
onWheel={(e) => e.stopPropagation()}
>
{/* Breadcrumbs Header */}
<div className="px-2 py-1 text-void-fg-3 bg-void-bg-2-alt text-sm border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionPath.length || optionText ?
{areBreadcrumbsShowing && <div className="px-2 py-1 text-void-fg-1 bg-void-bg-2-alt border-b border-void-border-3 sticky top-0 bg-void-bg-1 z-10 select-none pointer-events-none">
{optionText ?
<div className="flex items-center">
{optionPath.map((path, index) => (
{/* {optionPath.map((path, index) => (
<React.Fragment key={index}>
<span>{path}</span>
<ChevronRight size={12} className="mx-1" />
</React.Fragment>
))}
))} */}
<span>{optionText}</span>
</div>
: <div className='opacity-60'>Enter text to filter...</div>
: <div className='opacity-50'>Enter text to filter...</div>
}
</div>
</div>}
{/* Options list */}
<div className='max-h-[400px] w-full max-w-full overflow-y-auto overflow-x-auto'>
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap text-sm opacity-70">
<div className="w-max min-w-full flex flex-col gap-0 text-nowrap flex-nowrap">
{options.length === 0 ?
<div className="text-void-fg-3 px-3 py-0.5">No results found</div>
: options.map((o, oIdx) => {
@ -767,17 +809,19 @@ export const VoidInputBox2 = forwardRef<HTMLTextAreaElement, InputBox2Props>(fun
// Option
<div
ref={oIdx === optionIdx ? selectedOptionRef : null}
key={o.nameInMenu}
key={o.fullName}
className={`
flex items-center gap-2
px-3 py-0.5 cursor-pointer bg-void-bg-2-alt
px-3 py-1 cursor-pointer bg-void-bg-2-alt
${oIdx === optionIdx ? 'bg-void-bg-2-hover' : ''}
`}
onClick={() => { onSelectOption(); }}
onMouseOver={() => { setOptionIdx(oIdx) }}
onMouseMove={() => { setOptionIdx(oIdx) }}
>
{<o.iconInMenu size={12} />}
<span className="text-void-fg-1">{o.nameInMenu}</span>
<span className="text-void-fg-1">{o.abbreviatedName}</span>
{o.fullName && o.fullName !== o.abbreviatedName && <span className="text-void-fg-1 opacity-60 text-sm">{o.fullName}</span>}
{o.nextOptions || o.generateNextOptions ? (
<ChevronRight size={12} />
) : null}

View file

@ -1053,7 +1053,7 @@ export const Settings = () => {
<div className='w-full'>
<h4 className={`text-base`}>{displayInfoOfFeatureName('Apply')}</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button and the Edit tool.</div>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>Settings that control the behavior of the Apply button.</div>
<div className='my-2'>
{/* Sync to Chat Switch */}
@ -1126,7 +1126,7 @@ export const Settings = () => {
<div className='w-full'>
<h4 className={`text-base`}>Editor</h4>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of suggestions and widgets in the code editor.`}</div>
<div className='text-sm italic text-void-fg-3 mt-1 mb-4'>{`Settings that control the visibility of Void suggestions in the code editor.`}</div>
<div className='my-2'>
{/* Auto Accept Switch */}
@ -1162,10 +1162,9 @@ export const Settings = () => {
{/* Import/Export section, as its own block right after One-Click Switch */}
<div className='mt-12'>
<h2 className='text-3xl mb-2'>Import/Export</h2>
<div className='flex gap-8'>
<div className='flex flex-col gap-8'>
{/* Settings Subcategory */}
<div className='flex flex-col gap-2 max-w-48 w-full'>
<h3 className='text-xl mb-2'>Settings</h3>
<input key={2 * s} ref={fileInputSettingsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Settings')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputSettingsRef.current?.click() }}>
Import Settings
@ -1179,7 +1178,6 @@ export const Settings = () => {
</div>
{/* Chats Subcategory */}
<div className='flex flex-col gap-2 w-full max-w-48'>
<h3 className='text-xl mb-2'>Chat</h3>
<input key={2 * s + 1} ref={fileInputChatsRef} type='file' accept='.json' className='hidden' onChange={handleUpload('Chats')} />
<VoidButtonBgDarken className='px-4 py-1 w-full' onClick={() => { fileInputChatsRef.current?.click() }}>
Import Chats

View file

@ -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.`)
},

View file

@ -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);

View file

@ -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: ['<think>', '</think>'] },
contextWindow: 128_000, maxOutputTokens: 8_192,
},
'qwen3': {
supportsFIM: false, // replaces QwQ
supportsSystemMessage: 'system-role',
reasoningCapabilities: { supportsReasoning: true, canTurnOffReasoning: true, canIOReasoning: true, openSourceThinkTags: ['<think>', '</think>'] },
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 }

View file

@ -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<T extends Record<string, any>> = {
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()}.`)

View file

@ -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

View file

@ -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 },

View file

@ -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<ModelResponse> = ModelListParams<ModelResponse>
@ -42,13 +48,6 @@ const invalidApiKeyMessage = (providerName: ProviderName) => `Invalid ${displayI
// ------------ OPENAI-COMPATIBLE (HELPERS) ------------
// const getGoogleApiKey = async () => {
// // modulelevel 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,
},

View file

@ -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 })