mirror of
https://github.com/voideditor/void
synced 2026-05-24 09:58:23 +00:00
commit
c5bf5e453e
17 changed files with 742 additions and 431 deletions
|
|
@ -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
8
package-lock.json
generated
|
|
@ -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"
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
||||
|
|
|
|||
|
|
@ -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 };
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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'>
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
|
|
@ -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.`)
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
|
|
|
|||
|
|
@ -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 }
|
||||
|
|
|
|||
|
|
@ -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()}.`)
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
||||
|
||||
|
||||
|
|
|
|||
|
|
@ -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 },
|
||||
|
|
|
|||
|
|
@ -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 () => {
|
||||
// // 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,
|
||||
},
|
||||
|
|
|
|||
|
|
@ -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 })
|
||||
|
||||
|
|
|
|||
Loading…
Reference in a new issue