From 6a6cb56d87a1184712b00ec782cb750868bcd64a Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Fri, 21 Mar 2025 05:50:31 -0700 Subject: [PATCH] add visual feedback for tool that's being loaded (eg edit tool) --- .../contrib/void/browser/chatThreadService.ts | 8 +- .../contrib/void/browser/editCodeService.ts | 22 ++++- .../react/src/sidebar-tsx/SidebarChat.tsx | 92 +++++++++++-------- .../common/helpers/extractCodeFromResult.ts | 12 +-- .../void/common/sendLLMMessageTypes.ts | 2 +- .../llmMessage/sendLLMMessage.impl.ts | 30 ++++-- 6 files changed, 109 insertions(+), 57 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 861e1847..6eed5b16 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -117,6 +117,8 @@ export type ThreadStreamState = { streamingToken?: string; messageSoFar?: string; reasoningSoFar?: string; + toolNameSoFar?: string; + toolParamsSoFar?: string; } } @@ -874,11 +876,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { modelSelection, modelSelectionOptions, logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, - onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') }, + onText: ({ fullText, fullReasoning, fullToolName, fullToolParams }) => { + this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning, toolNameSoFar: fullToolName, toolParamsSoFar: fullToolParams }, 'merge') + }, onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) // added to history and no longer streaming this, so clear messages so far and streamingToken (but do not stop isRunning) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, }, 'merge') + this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, toolNameSoFar: undefined, toolParamsSoFar: undefined }, 'merge') // resolve with tool calls resMessageIsDonePromise(toolCalls) }, diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index fc9f1496..abb0bf24 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -1695,14 +1695,17 @@ 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) // if error if (typeof originalBounds === 'string') { - console.log('Error finding text in code:') + console.log('--------------Error finding text in code:') console.log('originalFileCode', { originalFileCode }) console.log('fullText', { fullText }) console.log('error:', originalBounds) console.log('block.orig:', block.orig) + console.log('---------') const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks) messages.push( { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output @@ -1710,10 +1713,14 @@ class EditCodeService extends Disposable implements IEditCodeService { ) // REVERT ALL BLOCKS + currStreamingBlockNum = 0 latestStreamLocationMutable = null shouldUpdateOrigStreamStyle = true oldBlocks = [] + for (const trackingZone of addedTrackingZoneOfBlockNum) + this._deleteTrackingZone(trackingZone) addedTrackingZoneOfBlockNum.splice(0, Infinity) + this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true }) // abort and resolve @@ -1729,7 +1736,14 @@ class EditCodeService extends Disposable implements IEditCodeService { return } + console.log('---------adding-------') + console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + console.log('block', deepClone(block)) + console.log('origBounds', originalBounds) + + const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) + console.log('start end', startLine, endLine) // otherwise if no error, add the position as a diffarea const adding: Omit, 'diffareaid'> = { @@ -1802,9 +1816,9 @@ class EditCodeService extends Disposable implements IEditCodeService { addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) const { model } = this._voidModelService.getModel(uri) - console.log('CURRENT!!!', { current: model?.getValue() }) - console.log('ADDED', addedTrackingZoneOfBlockNum) - console.log('BLOX', blocks) + console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum) + console.log('blocks', deepClone(blocks)) for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata 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 99d6bf62..f887dea6 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 @@ -27,7 +27,7 @@ import { WarningBox } from '../void-settings-tsx/WarningBox.js'; import { getModelCapabilities, getIsResoningEnabledState } from '../../../../common/modelCapabilities.js'; import { AlertTriangle, Ban, ChevronRight, Dot, Pencil, X } from 'lucide-react'; import { ChatMessage, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../../../../common/chatThreadServiceTypes.js'; -import { ToolCallParams, ToolName, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; +import { ToolCallParams, ToolName, toolNames, ToolNameWithApproval } from '../../../../common/toolsServiceTypes.js'; import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; import { IsRunningType } from '../../../chatThreadService.js'; @@ -1040,7 +1040,7 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => { {children} } -const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType }) => { +const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, isToolBeingWritten }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType, isToolBeingWritten: boolean }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -1058,7 +1058,8 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas } const isEmpty = !chatMessage.content && !chatMessage.reasoning - const isLastAndLoading = !isCommitted && isLast && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user') + const isLoading = !isCommitted && !isToolBeingWritten && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user') + const isLastAndLoading = isLast && isLoading if (isEmpty && !isLastAndLoading) return null return <> @@ -1083,7 +1084,7 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas isLinkDetectionEnabled={true} /> {/* loading indicator */} - {!isCommitted && } + {isLoading && } @@ -1117,19 +1118,19 @@ const loadingTitleWrapper = (item: React.ReactNode) => { } const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' -const toolNameToTitle = { +const titleOfToolName = { 'read_file': { done: 'Read file', proposed: 'Read file', running: loadingTitleWrapper('Reading file') }, 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: loadingTitleWrapper('Inspecting folder') }, 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: loadingTitleWrapper('Searching by file name') }, 'text_search': { done: 'Searched', proposed: 'Search text', running: loadingTitleWrapper('Searching') }, 'create_uri': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, - proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean | null) => isFolder === null ? 'Create URI' : `Create ${folderFileStr(isFolder)}`, running: (isFolder: boolean) => loadingTitleWrapper(`Creating ${folderFileStr(isFolder)}`) }, 'delete_uri': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, - proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean | null) => isFolder === null ? 'Delete URI' : `Delete ${folderFileStr(isFolder)}`, running: (isFolder: boolean) => loadingTitleWrapper(`Deleting ${folderFileStr(isFolder)}`) }, 'edit': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, @@ -1259,7 +1260,7 @@ export const ToolChildrenWrapper = ({ children, className }: { children: React.R } -export const ErrorChildren = ({ children }: { children: React.ReactNode }) => { +export const CodeChildren = ({ children }: { children: React.ReactNode }) => { return
{children} @@ -1315,7 +1316,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const { uri } = toolMessage.result.params ?? {} const desc1 = uri ? getBasename(uri.fsPath) : ''; const icon = null @@ -1334,9 +1335,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { value, params } = toolMessage.result if (params) componentParams.desc2 = componentParams.children = - + {value} - + } @@ -1349,7 +1350,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1381,9 +1382,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else { const { value, params } = toolMessage.result componentParams.children = - + {value} - + } @@ -1396,7 +1397,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1424,9 +1425,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else { const { value, params } = toolMessage.result componentParams.children = - + {value} - + } @@ -1439,7 +1440,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1467,9 +1468,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { else { const { value, params } = toolMessage.result componentParams.children = - + {value} - + } return @@ -1485,7 +1486,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const explorerService = accessor.get('IExplorerService') const isError = false const isFolder = toolRequest.params.isFolder - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1499,7 +1500,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' const isFolder = toolMessage.result.params?.isFolder ?? false - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1517,9 +1518,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { params, value } = toolMessage.result if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = - + {value} - + } @@ -1532,7 +1533,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const commandService = accessor.get('ICommandService') const isError = false const isFolder = toolRequest.params.isFolder - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed(isFolder) : titleOfToolName[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1549,7 +1550,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const isFolder = toolMessage.result.params?.isFolder ?? false const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done(isFolder) : toolNameToTitle[toolMessage.name].proposed(isFolder) + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done(isFolder) : titleOfToolName[toolMessage.name].proposed(isFolder) const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1567,9 +1568,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const { params, value } = toolMessage.result if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } componentParams.children = componentParams.children = - + {value} - + } @@ -1580,7 +1581,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => { const accessor = useAccessor() const isError = false - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1602,7 +1603,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const accessor = useAccessor() const isError = toolMessage.result.type === 'error' const isRejected = toolMessage.result.type === 'rejected' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1641,9 +1642,9 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { if (params) { componentParams.children = {/* error */} - + {value} - + {/* content */} } = { } else { - componentParams.children = + componentParams.children = {value} - + } } } @@ -1669,7 +1670,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') const isError = false - const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running + const title = toolRequestState === 'awaiting_user' ? titleOfToolName[toolRequest.name].proposed : titleOfToolName[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1688,7 +1689,7 @@ const toolNameToComponent: { [T in ToolName]: ToolComponent } = { const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') const isError = toolMessage.result.type === 'error' - const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].done : toolNameToTitle[toolMessage.name].proposed + const title = toolMessage.result.type === 'success' ? titleOfToolName[toolMessage.name].done : titleOfToolName[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null @@ -1753,9 +1754,10 @@ type ChatBubbleProps = { isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message) chatIsRunning: IsRunningType, threadId: string, + isToolBeingWritten: boolean, } -const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId }: ChatBubbleProps) => { +const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId, isToolBeingWritten }: ChatBubbleProps) => { const role = chatMessage.role if (role === 'user') { @@ -1772,6 +1774,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin isCommitted={isCommitted} chatIsRunning={chatIsRunning} isLast={isLast} + isToolBeingWritten={isToolBeingWritten} /> } else if (role === 'tool_request') { @@ -1838,6 +1841,10 @@ export const SidebarChat = () => { const messageSoFar = currThreadStreamState?.messageSoFar const reasoningSoFar = currThreadStreamState?.reasoningSoFar + const toolNameSoFar = currThreadStreamState?.toolNameSoFar + const toolParamsSoFar = currThreadStreamState?.toolParamsSoFar + const toolIsLoading = !!toolNameSoFar && toolNameSoFar === 'edit' // show loading for slow tools (right now just edit) + // ----- SIDEBAR CHAT state (local) ----- // state of current message @@ -1902,6 +1909,7 @@ export const SidebarChat = () => { chatIsRunning={isRunning} isLast={isLast} threadId={threadId} + isToolBeingWritten={toolIsLoading} /> }) }, [previousMessages, isRunning, currentThread, numMessages]) @@ -1921,9 +1929,17 @@ export const SidebarChat = () => { chatIsRunning={isRunning} isLast={true} threadId={threadId} + isToolBeingWritten={toolIsLoading} /> : null - const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML] + + const proposed = toolNameSoFar && toolNames.includes(toolNameSoFar as ToolName) ? titleOfToolName[toolNameSoFar as ToolName]?.proposed : toolNameSoFar + const toolTitle = typeof proposed === 'function' ? proposed(null) : proposed + const currStreamingToolHTML = toolIsLoading ? + } /> + : null + + const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML, currStreamingToolHTML] const threadSelector =
{ + const newOnText: OnText = ({ fullText: fullText_, ...p }) => { // until found the first think tag, keep adding to fullText if (!foundTag1) { const endsWithTag1 = endsWithAnyPrefixOf(fullText_, thinkTags[0]) @@ -282,7 +282,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string fullTextSoFar += fullText_.substring(0, tag1Index) // Update latestAddIdx to after the first tag latestAddIdx = tag1Index + thinkTags[0].length - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -290,7 +290,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string // add the text to fullText fullTextSoFar = fullText_ latestAddIdx = fullText_.length - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -314,7 +314,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string fullReasoningSoFar += fullText_.substring(latestAddIdx, tag2Index) // Update latestAddIdx to after the second tag latestAddIdx = tag2Index + thinkTags[1].length - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -327,7 +327,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string latestAddIdx = fullText_.length } - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) return } @@ -340,7 +340,7 @@ export const extractReasoningOnTextWrapper = (onText: OnText, thinkTags: [string latestAddIdx = fullText_.length } - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ ...p, fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) } return newOnText diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index f5660924..82df3d26 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -54,7 +54,7 @@ export type ToolCallType = { export type AnthropicReasoning = ({ type: 'thinking'; thinking: any; signature: string; } | { type: 'redacted_thinking', data: any }) -export type OnText = (p: { fullText: string; fullReasoning: string }) => void +export type OnText = (p: { fullText: string; fullReasoning: string; fullToolName: string; fullToolParams: string; }) => void export type OnFinalMessage = (p: { fullText: string; fullReasoning: string; toolCalls?: ToolCallType[]; anthropicReasoning: AnthropicReasoning[] | null }) => void // id is tool_use_id export type OnError = (p: { message: string; fullError: Error | null }) => void export type OnAbort = () => void diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts index 9e82f58c..1d61af41 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.impl.ts @@ -195,6 +195,10 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage let fullReasoningSoFar = '' let fullTextSoFar = '' + + let fullToolName = '' + let fullToolParams = '' + const toolCallOfIndex: ToolCallOfIndex = {} openai.chat.completions .create(options) @@ -209,6 +213,9 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage toolCallOfIndex[index].name += tool.function?.name ?? '' toolCallOfIndex[index].paramsStr += tool.function?.arguments ?? ''; toolCallOfIndex[index].id += tool.id ?? '' + + fullToolName += tool.function?.name ?? '' + fullToolParams += tool.function?.arguments ?? '' } // message const newText = chunk.choices[0]?.delta?.content ?? '' @@ -222,7 +229,7 @@ const _sendOpenAICompatibleChat = ({ messages: messages_, onText, onFinalMessage fullReasoningSoFar += newReasoning } - onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar }) + onText({ fullText: fullTextSoFar, fullReasoning: fullReasoningSoFar, fullToolName, fullToolParams }) } // on final const toolCalls = toolCallsFrom_OpenAICompat(toolCallOfIndex) @@ -351,6 +358,9 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM let fullText = '' let fullReasoning = '' + let fullToolName = '' + let fullToolParams = '' + // there are no events for tool_use, it comes in at the end stream.on('streamEvent', e => { // start block @@ -358,18 +368,22 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM if (e.content_block.type === 'text') { if (fullText) fullText += '\n\n' // starting a 2nd text block fullText += e.content_block.text - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } else if (e.content_block.type === 'thinking') { if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block fullReasoning += e.content_block.thinking - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } else if (e.content_block.type === 'redacted_thinking') { console.log('delta', e.content_block.type) if (fullReasoning) fullReasoning += '\n\n' // starting a 2nd reasoning block fullReasoning += '[redacted_thinking]' - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) + } + else if (e.content_block.type === 'tool_use') { + fullToolName += e.content_block.name ?? '' // anthropic gives us the tool name in the start block + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } } @@ -377,11 +391,15 @@ const sendAnthropicChat = ({ messages: messages_, providerName, onText, onFinalM else if (e.type === 'content_block_delta') { if (e.delta.type === 'text_delta') { fullText += e.delta.text - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } else if (e.delta.type === 'thinking_delta') { fullReasoning += e.delta.thinking - onText({ fullText, fullReasoning }) + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) + } + else if (e.delta.type === 'input_json_delta') { // tool use + fullToolParams += e.delta.partial_json ?? '' // anthropic gives us the partial delta (string) here - https://docs.anthropic.com/en/api/messages-streaming + onText({ fullText, fullReasoning, fullToolName, fullToolParams }) } } })