diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3f2b7e52..8772dc23 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -106,10 +106,11 @@ export type ThreadsState = { currentThreadId: string; // intended for internal use only } +export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user' export type ThreadStreamState = { [threadId: string]: undefined | { // state related - isRunning?: undefined | 'message' | 'tool'; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response) + isRunning?: IsRunningType; // whether or not actually running the agent loop (can be running and not streaming, like if it's calling a tool and awaiting user response) error?: { message: string, fullError: Error | null, }; // streaming related @@ -650,6 +651,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen + this._cancelToolOfThreadId[threadId]?.() + const lastMessage = thread.messages[thread.messages.length - 1] if (lastMessage.role !== 'tool_request') return // should never happen const { name, params, paramsStr, id } = lastMessage @@ -665,6 +668,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + // abort the stream first so it doesn't change any state + const llmCancelToken = this.streamState[threadId]?.streamingToken + if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } + + // add the correct message to the state const lastMessage = thread.messages[thread.messages.length - 1] if (lastMessage.role === 'tool_request') { // interrupt tool request @@ -675,9 +683,6 @@ class ChatThreadService extends Disposable implements IChatThreadService { this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) } - const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - this._setStreamState(threadId, {}, 'set') } @@ -701,6 +706,8 @@ class ChatThreadService extends Disposable implements IChatThreadService { } + private readonly _cancelToolOfThreadId: { [threadId: string]: (() => void) | undefined } = {} + private async _chatAgentLoop({ threadId, prevSelns, @@ -754,7 +761,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const handleToolCall = async ( tool: ToolCallType, opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, - ): Promise => { + ): Promise<{ awaitingUserApproval: boolean, canceled: boolean }> => { const toolName: ToolName = tool.name const toolParamsStr = tool.paramsStr const toolId = tool.id @@ -772,14 +779,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: undefined, value: errorMessage }, }) - return false + return { awaitingUserApproval: false, canceled: false } } // 2. if tool requires approval, break from the loop, awaiting approval const requiresApproval = !this._settingsService.state.globalSettings.autoApprove if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) { this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) - return true + return { awaitingUserApproval: true, canceled: false } } } else { @@ -788,12 +795,20 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') + let canceled = false try { - toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... - } catch (error) { + const { result, cancel } = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... + this._cancelToolOfThreadId[threadId] = cancel + let cancelRes: () => void = () => { } + const resolveIfCancel = new Promise((res, rej) => { cancelRes = rej }) + this._cancelToolOfThreadId[threadId] = () => { cancel?.(); canceled = true; delete this._cancelToolOfThreadId[threadId]; cancelRes() } + toolResult = await Promise.race([result, resolveIfCancel]) // this await is needed, typescript is bad... + } + catch (error) { + if (canceled) return { awaitingUserApproval: false, canceled: true } const errorMessage = getErrorMessage(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - return false + return { awaitingUserApproval: true, canceled: false } } // 4. stringify the result to give to the LLM @@ -802,12 +817,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { } catch (error) { const errorMessage = this.errMsgs.errWhenStringifying(error) this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'error', params: toolParams, value: errorMessage }, }) - return false + return { awaitingUserApproval: false, canceled: false } } // 5. add to history and keep going this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: toolResultStr, result: { type: 'success', params: toolParams, value: toolResult }, }) - return false + return { awaitingUserApproval: false, canceled: false } }; // above just defines helpers, below starts the actual function @@ -819,13 +834,14 @@ class ChatThreadService extends Disposable implements IChatThreadService { let nMessagesSent = 0 let shouldSendAnotherMessage = true - let exitReason: 'end' | 'awaitingToolApproval' = 'end' as 'end' | 'awaitingToolApproval' + let isRunningWhenEnd: IsRunningType = undefined let aborted = false // before enter loop, call tool if (callThisToolFirst) { this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + const { canceled } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + if (canceled) return } // tool use loop @@ -834,7 +850,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // false by default each iteration shouldSendAnotherMessage = false - exitReason = 'end' + isRunningWhenEnd = undefined nMessagesSent += 1 @@ -860,9 +876,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { // call tool if there is one const tool: ToolCallType | undefined = toolCalls?.[0] if (tool) { - const awaitingUserApproval = await handleToolCall(tool) + const { awaitingUserApproval } = await handleToolCall(tool) // things happen correctly if canceled is true here, because canceled calls onAbort if (awaitingUserApproval) { - exitReason = 'awaitingToolApproval' + isRunningWhenEnd = 'awaiting_user' } else { shouldSendAnotherMessage = true } @@ -900,9 +916,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { if (aborted) { return } } // end while + // if awaiting user approval, keep isRunning true, else end isRunning - if (exitReason === 'end') - this._setStreamState(threadId, { isRunning: undefined }, 'merge') + this._setStreamState(threadId, { isRunning: isRunningWhenEnd }, 'merge') // capture number of messages sent this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode }) @@ -1012,7 +1028,13 @@ class ChatThreadService extends Disposable implements IChatThreadService { } // else search codebase for `target` - const { uris } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) + let uris: URI[] = [] + try { + const { result } = await this._toolsService.callTool['pathname_search']({ queryStr: target, pageNumber: 0 }) + uris = result.uris + } catch (e) { + return null + } for (const [idx, uri] of uris.entries()) { if (doesUriMatchTarget(uri)) { 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 34d269c3..bff5576a 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 @@ -5,7 +5,7 @@ import { isFeatureNameDisabled } from '../../../../common/voidSettingsTypes.js' import { URI } from '../../../../../../../base/common/uri.js' import { FileSymlink, LucideIcon, RotateCw } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' -import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' +import { getBasename, ListableToolItem, ToolChildrenWrapper } from '../sidebar-tsx/SidebarChat.js' import { ChatMarkdownRender } from './ChatMarkdownRender.js' enum CopyButtonText { @@ -344,9 +344,9 @@ export const BlockCodeApplyWrapper = ({ {/* contents */} - + {children} - + } 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 338ec015..bac48dda 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 @@ -31,6 +31,7 @@ import { ResolveReason, ToolCallParams, ToolName, ToolNameWithApproval } from '. import { JumpToFileButton, useApplyButtonHTML } from '../markdown/ApplyBlockHoverButtons.js'; import { DiffZone } from '../../../editCodeService.js'; import { ScrollType } from '../../../../../../../editor/common/editorCommon.js'; +import { IsRunningType } from '../../../chatThreadService.js'; @@ -1096,11 +1097,11 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe if (!isWriting) setIsOpen(false) // if just finished reasoning, close }, [isWriting]) return : ''} isOpen={isOpen} onClick={() => setIsOpen(v => !v)}> - +
{children}
-
+
} @@ -1111,15 +1112,25 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe const folderFileStr = (isFolder: boolean) => isFolder ? 'folder' : 'file' const toolNameToTitle = { - 'read_file': { done: 'Read file', proposed: 'Read file' }, - 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder' }, - 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name' }, - 'text_search': { done: 'Searched', proposed: 'Search text' }, - 'create_uri': { done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}` }, - 'delete_uri': { done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}` }, - 'edit': { done: 'Edited file', proposed: 'Edit file' }, - 'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command' } -} as const satisfies Record + 'read_file': { done: 'Read file', proposed: 'Read file', running: 'Reading file...' }, + 'list_dir': { done: 'Inspected folder', proposed: 'Inspect folder', running: 'Inspecting folder...' }, + 'pathname_search': { done: 'Searched by file name', proposed: 'Search by file name', running: 'Searching by file name...' }, + 'text_search': { done: 'Searched', proposed: 'Search text', running: 'Searching...' }, + 'create_uri': { + done: (isFolder: boolean) => `Created ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean) => `Create ${folderFileStr(isFolder)}`, + running: (isFolder: boolean) => `Creating ${folderFileStr(isFolder)}...` + }, + 'delete_uri': { + done: (isFolder: boolean) => `Deleted ${folderFileStr(isFolder)}`, + proposed: (isFolder: boolean) => `Delete ${folderFileStr(isFolder)}`, + running: (isFolder: boolean) => `Deleting ${folderFileStr(isFolder)}...` + }, + 'edit': { done: 'Edited file', proposed: 'Edit file', running: 'Editing file...' }, + 'terminal_command': { done: 'Ran terminal command', proposed: 'Run terminal command', running: 'Running terminal command...' } +} as const satisfies Record + + const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1235,7 +1246,7 @@ const ToolRequestAcceptRejectButtons = () => { } -export const ToolContentsWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { +export const ToolChildrenWrapper = ({ children, className }: { children: React.ReactNode, className?: string }) => { return
{children} @@ -1266,21 +1277,25 @@ const EditToolApplyButton = ({ changeDescription, applyBoxId, uri }: { changeDes const EditToolChildren = ({ uri, changeDescription }: { uri: URI, changeDescription: string }) => { - return -
- - - -
-
+ return
+ + + +
} +type ToolRequestState = 'awaiting_user' | 'running' -const toolNameToComponent: { [T in ToolName]: { - requestWrapper: T extends ToolNameWithApproval ? ((props: { toolRequest: ToolRequestApproval }) => React.ReactNode) : null, - resultWrapper: (props: { toolMessage: ToolMessage, messageIdx: number }) => React.ReactNode, -} } = { +type RequestWrapper = (props: { toolRequest: ToolRequestApproval, messageIdx: number, toolRequestState: ToolRequestState, threadId: string }) => React.ReactNode +type ResultWrapper = (props: { toolMessage: ToolMessage, messageIdx: number, threadId: string }) => React.ReactNode + +type ToolComponent = { + requestWrapper: T extends ToolNameWithApproval ? RequestWrapper : null, + resultWrapper: ResultWrapper, +} + +const toolNameToComponent: { [T in ToolName]: ToolComponent } = { 'read_file': { requestWrapper: null, resultWrapper: ({ toolMessage }) => { @@ -1299,12 +1314,12 @@ const toolNameToComponent: { [T in ToolName]: { if (toolMessage.result.type === 'success') { const { value, params } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } - if (toolMessage.result.value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)` + if (value.hasNextPage) componentParams.desc2 = `(AI can scroll for more)` } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + if (params) componentParams.desc2 = + componentParams.children = {value} } return @@ -1330,7 +1345,7 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.numResults = value.children?.length componentParams.hasNextPage = value.hasNextPage componentParams.children = !value.children || (value.children.length ?? 0) === 0 ? undefined - : + : {value.children.map((child, i) => ( } - + } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + componentParams.children = {value} } return @@ -1373,7 +1387,7 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.numResults = value.uris.length componentParams.hasNextPage = value.hasNextPage componentParams.children = value.uris.length === 0 ? undefined - : + : {value.uris.map((uri, i) => ( } - + } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + componentParams.children = {value} } return @@ -1413,7 +1426,7 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.numResults = value.uris.length componentParams.hasNextPage = value.hasNextPage componentParams.children = value.uris.length === 0 ? undefined - : + : {value.uris.map((uri, i) => ( } - + } else { - componentParams.children = <> - {toolMessage.result.value} - + const { value, params } = toolMessage.result + componentParams.children = {value} } return } @@ -1437,12 +1449,13 @@ const toolNameToComponent: { [T in ToolName]: { // --- 'create_uri': { - requestWrapper: ({ toolRequest }) => { + requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const explorerService = accessor.get('IExplorerService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) + const isFolder = toolRequest.params.isFolder + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1463,7 +1476,7 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { - const { params } = toolMessage.result + const { params, value } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'rejected') { @@ -1471,20 +1484,21 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + const { params, value } = toolMessage.result + if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } + componentParams.children = {value} } return } }, 'delete_uri': { - requestWrapper: ({ toolRequest, }) => { + requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed(toolRequest.params.isFolder) + const isFolder = toolRequest.params.isFolder + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed(isFolder) : toolNameToTitle[toolRequest.name].running(isFolder) const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1508,7 +1522,7 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { - const { params } = toolMessage.result + const { params, value } = toolMessage.result componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'rejected') { @@ -1516,38 +1530,38 @@ const toolNameToComponent: { [T in ToolName]: { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + const { params, value } = toolMessage.result + if (params) { componentParams.onClick = () => { commandService.executeCommand('vscode.open', params.uri, { preview: true }) } } + componentParams.children = {value} } return } }, 'edit': { - requestWrapper: ({ toolRequest, }) => { + requestWrapper: ({ toolRequest, messageIdx, toolRequestState, threadId }) => { const accessor = useAccessor() - const commandService = accessor.get('ICommandService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null const componentParams: ToolHeaderParams = { title, desc1, isError, icon, } const { params } = toolRequest - componentParams.children = + componentParams.children = + + componentParams.desc2 = return }, - resultWrapper: ({ toolMessage, messageIdx }) => { + resultWrapper: ({ toolMessage, messageIdx, threadId }) => { const accessor = useAccessor() - const chatThreadsService = accessor.get('IChatThreadService') 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 @@ -1556,42 +1570,58 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } - if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') { + if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') { const { params } = toolMessage.result - const threadId = chatThreadsService.state.currentThreadId - const applyBoxId = getApplyBoxId({ - threadId: threadId, - messageIdx: messageIdx, - tokenIdx: 'N/A', - }) + // add apply box + if (params) { + const applyBoxId = getApplyBoxId({ + threadId: threadId, + messageIdx: messageIdx, + tokenIdx: 'N/A', + }) + componentParams.desc2 = + } - componentParams.children = - componentParams.desc2 = - } - else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + // add children + if (toolMessage.result.type !== 'error') { + const { params } = toolMessage.result + componentParams.children = + + + } + else { + // error + const { params, value } = toolMessage.result + if (params) { + componentParams.children = + {value} + + + } + } } return } }, 'terminal_command': { - requestWrapper: ({ toolRequest, }) => { + requestWrapper: ({ toolRequest, toolRequestState }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') const isError = false - const title = toolNameToTitle[toolRequest.name].proposed + const title = toolRequestState === 'awaiting_user' ? toolNameToTitle[toolRequest.name].proposed : toolNameToTitle[toolRequest.name].running const desc1 = toolNameToDesc(toolRequest.name, toolRequest.params) const icon = null @@ -1618,8 +1648,9 @@ const toolNameToComponent: { [T in ToolName]: { const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { - const { command } = toolMessage.result.params - const { terminalId, resolveReason, result } = toolMessage.result.value + const { value, params } = toolMessage.result + const { command } = params + const { terminalId, resolveReason, result } = value const resultStr = resolveReason.type === 'done' ? (resolveReason.exitCode !== 0 ? `\nError: exit code ${resolveReason.exitCode}` : null) : resolveReason.type === 'bgtask' ? null : @@ -1627,7 +1658,7 @@ const toolNameToComponent: { [T in ToolName]: { resolveReason.type === 'toofull' ? `\n(truncated)` : null - componentParams.children = + componentParams.children = - + if (resolveReason.type === 'bgtask') componentParams.desc2 = '(background task)' } - else if (toolMessage.result.type === 'rejected') { - const { proposedTerminalId, waitForCompletion } = toolMessage.result.params - if (terminalToolsService.terminalExists(proposedTerminalId)) - componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) - if (!waitForCompletion) - componentParams.desc2 = '(background task)' - } - else if (toolMessage.result.type === 'error') { - componentParams.children = <> - {toolMessage.result.value} - + else if (toolMessage.result.type === 'rejected' || toolMessage.result.type === 'error') { + const { params } = toolMessage.result + if (params) { + const { proposedTerminalId, waitForCompletion } = params + if (terminalToolsService.terminalExists(proposedTerminalId)) + componentParams.onClick = () => terminalToolsService.openTerminal(proposedTerminalId) + if (!waitForCompletion) + componentParams.desc2 = '(background task)' + } + if (toolMessage.result.type === 'error') { + const { value } = toolMessage.result + componentParams.children = {value} + } } return @@ -1670,9 +1703,11 @@ type ChatBubbleProps = { messageIdx: number, isCommitted: boolean, isLast: boolean, // includes the streaming message (if streaming, isLast is false except for the streaming message) + chatIsRunning: IsRunningType, + threadId: string, } -const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast }: ChatBubbleProps) => { +const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning, threadId }: ChatBubbleProps) => { const role = chatMessage.role if (role === 'user') { @@ -1691,18 +1726,23 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast }: ChatBubble /> } else if (role === 'tool_request') { - const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... + const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as RequestWrapper + const toolRequestType = ( + chatIsRunning === 'awaiting_user' ? 'awaiting_user' + : chatIsRunning === 'tool' ? 'running' + : null + ) if (ToolRequestWrapper && isLast) { // if it's the last message return <> - - + {toolRequestType !== null && } + {chatIsRunning === 'awaiting_user' && } } return null } else if (role === 'tool') { - const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as React.FC<{ toolMessage: any, messageIdx: number }> // ts isnt smart enough... - return + const ToolResultWrapper = toolNameToComponent[chatMessage.name].resultWrapper as ResultWrapper + return } } @@ -1801,18 +1841,22 @@ export const SidebarChat = () => { const numMessages = previousMessages.length const previousMessagesHTML = useMemo(() => { + const threadId = currentThread.id return previousMessages.map((message, i) => { - const isLast = i === numMessages - 1 && isRunning !== 'tool' + const isLast = i === numMessages - 1 return } ) }, [previousMessages, isRunning, currentThread, numMessages]) + const threadId = currentThread.id const streamingChatIdx = previousMessagesHTML.length const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ? { }} messageIdx={streamingChatIdx} isCommitted={!isRunning} + chatIsRunning={isRunning} isLast={true} + threadId={threadId} /> : null const allMessagesHTML = [...previousMessagesHTML, currStreamingMessageHTML] diff --git a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx index 074e7e09..9cc33081 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/void-command-bar-tsx/VoidCommandBar.tsx @@ -55,7 +55,6 @@ const VoidCommandBar = ({ uri, editor }: { uri: URI | null, editor: ICodeEditor const [currUriHasChanges, setCurrUriHasChanges] = useState(false) const anyUriHasChanges = sortedCommandBarURIs.length !== 0 useEffect(() => { - console.log('uri', uri?.fsPath, sortedCommandBarURIs) const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) if (i !== -1) { setUriIdx(i) diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index 030085ca..c43fe5af 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -20,7 +20,7 @@ import { basename } from '../../../../base/common/path.js' type ValidateParams = { [T in ToolName]: (p: string) => Promise } -type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], cancel?: () => void }> } type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } @@ -179,7 +179,6 @@ export class ToolsService implements IToolsService { public callTool: CallTool; public stringOfResult: ToolResultToString; - constructor( @IFileService fileService: IFileService, @IWorkspaceContextService workspaceContextService: IWorkspaceContextService, @@ -283,12 +282,12 @@ export class ToolsService implements IToolsService { const fileContents = readFileContents.slice(fromIdx, toIdx + 1) // paginate const hasNextPage = (readFileContents.length - 1) - toIdx >= 1 - return { fileContents, hasNextPage } + return { result: { fileContents, hasNextPage } } }, list_dir: async ({ rootURI, pageNumber }) => { const dirResult = await computeDirectoryResult(fileService, rootURI, pageNumber) - return dirResult + return { result: dirResult } }, pathname_search: async ({ queryStr, pageNumber }) => { @@ -304,7 +303,7 @@ export class ToolsService implements IToolsService { .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - return { uris, hasNextPage } + return { result: { uris, hasNextPage } } }, text_search: async ({ queryStr, pageNumber }) => { @@ -322,7 +321,7 @@ export class ToolsService implements IToolsService { .map(({ resource, results }) => resource) const hasNextPage = (data.results.length - 1) - toIdx >= 1 - return { queryStr, uris, hasNextPage } + return { result: { queryStr, uris, hasNextPage } } }, // --- @@ -333,12 +332,12 @@ export class ToolsService implements IToolsService { else { await fileService.createFile(uri) } - return {} + return { result: {} } }, delete_uri: async ({ uri, isRecursive }) => { await fileService.del(uri, { recursive: isRecursive }) - return {} + return { result: {} } }, edit: async ({ uri, changeDescription }) => { @@ -350,13 +349,15 @@ export class ToolsService implements IToolsService { startBehavior: 'keep-conflicts', }) if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) - const [_, applyDonePromise] = res - await applyDonePromise - return {} + const [diffZoneURI, applyDonePromise] = res + + const cancel = () => editCodeService.interruptURIStreaming({ uri: diffZoneURI }) + + return { result: applyDonePromise, cancel } }, terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) - return { terminalId, didCreateTerminal, result, resolveReason } + return { result: { terminalId, didCreateTerminal, result, resolveReason } } }, } diff --git a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts index 091d87de..4b3b8645 100644 --- a/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts +++ b/src/vs/workbench/contrib/void/browser/voidCommandBarService.ts @@ -134,7 +134,6 @@ export class VoidCommandBarService extends Disposable implements IVoidCommandBar if (e.uri.fsPath !== uri.fsPath) continue // --- sortedURIs: delete if empty, add if not in state yet const diffZones = this._getDiffZonesOnURI(uri) - console.log('addordelete diffzone', uri.fsPath, diffZones) if (diffZones.length === 0) { this._deleteURIEntryFromState(uri) this._onDidChangeState.fire({ uri })