diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index db482108..40a87ce8 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -12,7 +12,7 @@ import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; import { chat_userMessageContent, ToolName, } from '../common/prompt/prompts.js'; -import { getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; +import { AnthropicReasoning, getErrorMessage, RawToolCallObj, RawToolParamsObj } from '../common/sendLLMMessageTypes.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; @@ -540,7 +540,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const { name, id, rawParams } = lastMsg - const errorMessage = this.errMsgs.rejected + const errorMessage = this.toolErrMsgs.rejected this._updateLatestTool(threadId, { role: 'tool', type: 'rejected', params: params, name: name, content: errorMessage, result: null, id, rawParams }) this._setStreamState(threadId, undefined) } @@ -557,7 +557,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // add tool that's running else if (this.streamState[threadId]?.isRunning === 'tool') { - const { toolName, toolParams, id, content, rawParams } = this.streamState[threadId].toolInfo + const { toolName, toolParams, id, content: content_, rawParams } = this.streamState[threadId].toolInfo + const content = content_ || this.toolErrMsgs.interrupted this._updateLatestTool(threadId, { role: 'tool', name: toolName, params: toolParams, id, content, rawParams, type: 'rejected', result: null }) } // reject the tool for the user if relevant @@ -581,8 +582,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { - private readonly errMsgs = { + private readonly toolErrMsgs = { rejected: 'Tool call was rejected by the user.', + interrupted: 'Tool call was interrupted by the user.', errWhenStringifying: (error: any) => `Tool call succeeded, but there was an error stringifying the output.\n${getErrorMessage(error)}` } @@ -671,7 +673,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { try { toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) } catch (error) { - const errorMessage = this.errMsgs.errWhenStringifying(error) + const errorMessage = this.toolErrMsgs.errWhenStringifying(error) this._updateLatestTool(threadId, { role: 'tool', type: 'tool_error', params: toolParams, result: errorMessage, name: toolName, content: errorMessage, id: toolId, rawParams: opts.unvalidatedToolParams }) return {} } @@ -749,8 +751,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { shouldRetryLLM = false nAttempts += 1 - let resMessageIsDonePromise: (res: { type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }) => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise<{ type: 'llmDone', toolCall?: RawToolCallObj } | { type: 'llmError', error?: { message: string; fullError: Error | null; } } | { type: 'llmAborted' }>((res, rej) => { resMessageIsDonePromise = res }) + type ResTypes = + | { type: 'llmDone', toolCall?: RawToolCallObj, info: { fullText: string, fullReasoning: string, anthropicReasoning: AnthropicReasoning[] | null } } + | { type: 'llmError', error?: { message: string; fullError: Error | null; } } + | { type: 'llmAborted' } + + let resMessageIsDonePromise: (res: ResTypes) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', @@ -765,9 +772,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._setStreamState(threadId, { isRunning: 'LLM', llmInfo: { displayContentSoFar: fullText, reasoningSoFar: fullReasoning, toolCallSoFar: toolCall ?? null }, interrupt: Promise.resolve(() => { if (llmCancelToken) this._llmMessageService.abort(llmCancelToken) }) }) }, onFinalMessage: async ({ fullText, fullReasoning, toolCall, anthropicReasoning, }) => { - this._addMessageToThread(threadId, { role: 'assistant', displayContent: fullText, reasoning: fullReasoning, anthropicReasoning }) - resMessageIsDonePromise({ type: 'llmDone', toolCall }) // resolve with tool calls - + resMessageIsDonePromise({ type: 'llmDone', toolCall, info: { fullText, fullReasoning, anthropicReasoning } }) // resolve with tool calls }, onError: async (error) => { resMessageIsDonePromise({ type: 'llmError', error: error }) @@ -826,11 +831,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { } } - - // llm res success - const { toolCall } = llmRes - this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative, for clarity + const { toolCall, info } = llmRes + + this._addMessageToThread(threadId, { role: 'assistant', displayContent: info.fullText, reasoning: info.fullReasoning, anthropicReasoning: info.anthropicReasoning }) + + this._setStreamState(threadId, { isRunning: 'idle', interrupt: 'not_needed' }) // just decorative for clarity // call tool if there is one if (toolCall) { diff --git a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts index 74d81593..1b7df00e 100644 --- a/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts +++ b/src/vs/workbench/contrib/void/browser/convertToLLMMessageService.ts @@ -17,6 +17,7 @@ import { IVoidModelService } from '../common/voidModelService.js'; import { URI } from '../../../../base/common/uri.js'; import { EndOfLinePreference } from '../../../../editor/common/model.js'; +export const EMPTY_MESSAGE = '(empty message)' @@ -36,7 +37,6 @@ type SimpleLLMMessage = { } -const EMPTY_MESSAGE = '(empty message)' const CHARS_PER_TOKEN = 4 // assume abysmal chars per token const TRIM_TO_LEN = 120 @@ -405,14 +405,24 @@ const prepareOpenAIOrAnthropicMessages = ({ // ================ no empty message ================ - for (const currMsg of llmMessages) { + for (let i = 0; i < llmMessages.length; i += 1) { + const currMsg: AnthropicOrOpenAILLMMessage = llmMessages[i] + const nextMsg: AnthropicOrOpenAILLMMessage | undefined = llmMessages[i + 1] + if (currMsg.role === 'tool') continue // if content is a string, replace string with empty msg - if (typeof currMsg.content === 'string') + if (typeof currMsg.content === 'string') { currMsg.content = currMsg.content || EMPTY_MESSAGE + } else { - // if content is an array, replace any empty text entries with empty msg, and make sure there's at least 1 entry + // allowed to be empty if has a tool in it or following it + if (currMsg.content.find(c => c.type === 'tool_result' || c.type === 'tool_use')) { + continue + } + if (nextMsg?.role === 'tool') continue + + // replace any empty text entries with empty msg, and make sure there's at least 1 entry for (const c of currMsg.content) { if (c.type === 'text') c.text = c.text || EMPTY_MESSAGE } diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx index b9e99042..6a4d393a 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ApplyBlockHoverButtons.tsx @@ -311,7 +311,6 @@ export const ApplyButtonsHTML = ({ const currStreamState = currStreamStateRef.current - console.log('currStreamState...', currStreamState) if (currStreamState === 'streaming') { return (false) let link: CodespanLocationLink | undefined = undefined - - if (!rawText.endsWith('`')) return null - - - // get link from cache - link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) - - if (link === undefined) { - // if no link, generate link and add to cache - chatThreadService.generateCodespanLink({ codespanStr: text, threadId }) - .then(link => { - chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) - setDidComputeCodespanLink(true) // rerender - }) - - } - - // If it's a file path, shorten it and add tooltip (whether or not it's a link) - let displayText = link?.displayText || text let tooltip: string | undefined = undefined - if (isValidUri(displayText)) { - tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip - displayText = getBasename(displayText) + let displayText = text + + + if (rawText.endsWith('`')) { + // get link from cache + link = chatThreadService.getCodespanLink({ codespanStr: text, messageIdx, threadId }) + + if (link === undefined) { + // if no link, generate link and add to cache + chatThreadService.generateCodespanLink({ codespanStr: text, threadId }) + .then(link => { + chatThreadService.addCodespanLink({ newLinkText: text, newLinkLocation: link, messageIdx, threadId }) + setDidComputeCodespanLink(true) // rerender + }) + } + + if (link?.displayText) { + displayText = link.displayText + } + + if (isValidUri(displayText)) { + tooltip = getRelative(URI.file(displayText), accessor) // Full path as tooltip + displayText = getBasename(displayText) + } } + const onClick = () => { if (!link) return; // Use the updated voidOpenFileFn to open the file and handle selection diff --git a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx index 52480137..0252381b 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/sidebar-tsx/SidebarChat.tsx @@ -500,7 +500,7 @@ export const getRelative = (uri: URI, accessor: ReturnType) let path: string const isInside = workspaceContextService.isInsideWorkspace(uri) if (isInside) { - const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath.startsWith(f.uri.fsPath)) + const f = workspaceContextService.getWorkspace().folders.find(f => uri.fsPath?.startsWith(f.uri.fsPath)) if (f) { path = uri.fsPath.replace(f.uri.fsPath, '') } else { path = uri.fsPath } } @@ -1651,8 +1651,8 @@ const LintErrorChildren = ({ lintErrors }: { lintErrors: LintErrorItem[] }) => { } const BottomChildren = ({ children, title }: { children: React.ReactNode, title: string }) => { - if (!children) return null; const [isOpen, setIsOpen] = useState(false); + if (!children) return null; return (
, } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren = @@ -1960,7 +1960,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren = @@ -2009,7 +2009,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren = @@ -2064,7 +2064,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, } - else { + else if (toolMessage.type === 'tool_error') { const { result } = toolMessage componentParams.bottomChildren =