From 97b44a94e44f9750353d50c948a536164dcbed82 Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 16 Mar 2025 02:54:56 -0700 Subject: [PATCH] redesign how tool use loop works --- .../void/browser/autocompleteService.ts | 6 +- .../contrib/void/browser/chatThreadService.ts | 379 ++++++++---------- .../src/markdown/ApplyBlockHoverButtons.tsx | 20 +- .../react/src/markdown/ChatMarkdownRender.tsx | 29 +- .../src/quick-edit-tsx/QuickEditChat.tsx | 1 - .../react/src/sidebar-tsx/SidebarChat.tsx | 139 +++---- .../void/common/chatThreadServiceTypes.ts | 2 +- .../contrib/void/common/prompt/prompts.ts | 6 +- .../void/common/sendLLMMessageTypes.ts | 4 +- .../void/common/voidSettingsService.ts | 2 +- .../contrib/void/common/voidSettingsTypes.ts | 2 +- .../llmMessage/sendLLMMessage.ts | 3 +- 12 files changed, 292 insertions(+), 301 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/autocompleteService.ts b/src/vs/workbench/contrib/void/browser/autocompleteService.ts index 6479fb6d..3a621e18 100644 --- a/src/vs/workbench/contrib/void/browser/autocompleteService.ts +++ b/src/vs/workbench/contrib/void/browser/autocompleteService.ts @@ -637,6 +637,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ token: CancellationToken, ): Promise { + const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete + if (!isEnabled) return [] + const testMode = false const docUriStr = model.uri.toString(); @@ -792,10 +795,9 @@ export class AutocompleteService extends Disposable implements IAutocompleteServ const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined - const isEnabled = this._settingsService.state.globalSettings.enableAutocomplete // set parameters of `newAutocompletion` appropriately - newAutocompletion.llmPromise = isEnabled ? new Promise((resolve, reject) => reject('Autocomplete is disabled')) : new Promise((resolve, reject) => { + newAutocompletion.llmPromise = new Promise((resolve, reject) => { const requestId = this._llmMessageService.sendLLMMessage({ messagesType: 'FIMMessage', diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 82abaebb..1447b7ff 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -26,6 +26,7 @@ import { ITextModelService } from '../../../../editor/common/services/resolverSe import { ChatMessage, CodespanLocationLink, StagingSelectionItem, ToolMessage, ToolRequestApproval } from '../common/chatThreadServiceTypes.js'; import { Position } from '../../../../editor/common/core/position.js'; import { ITerminalToolService } from './terminalToolService.js'; +import { IMetricsService } from '../common/metricsService.js'; const findLastIndex = (arr: T[], condition: (t: T) => boolean): number => { for (let i = arr.length - 1; i >= 0; i--) { @@ -104,10 +105,14 @@ export type ThreadsState = { export type ThreadStreamState = { [threadId: string]: undefined | { + // state related + isRunning?: undefined | true; // 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 + streamingToken?: string; messageSoFar?: string; reasoningSoFar?: string; - streamingToken?: string; } } @@ -135,7 +140,7 @@ export interface IChatThreadService { readonly _serviceBrand: undefined; readonly state: ThreadsState; - readonly streamState: ThreadStreamState; + readonly streamState: ThreadStreamState; // not persistent onDidChangeCurrentThread: Event; onDidChangeStreamState: Event<{ threadId: string }> @@ -167,18 +172,18 @@ export interface IChatThreadService { closeStagingSelectionsInMessage(messageIdx: number): void; - cancelStreaming(threadId: string): void; + stopRunning(threadId: string): void; dismissStreamError(threadId: string): void; // call to edit a message - CAN THROW - editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }): Promise; + editUserMessageAndStreamResponse({ userMessage, messageIdx }: { userMessage: string, messageIdx: number }): Promise; // call to add a message - CAN THROW - addUserMessageAndStreamResponse({ userMessage, chatMode }: { userMessage: string, chatMode: ChatMode }): Promise; + addUserMessageAndStreamResponse({ userMessage }: { userMessage: string }): Promise; // approve/reject - CAN THROW - approveTool(toolId: string): void; - rejectTool(toolId: string): void; + approveTool(threadId: string): void; + rejectTool(threadId: string): void; } export const IChatThreadService = createDecorator('voidChatThreadService'); @@ -205,6 +210,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { @ILanguageFeaturesService private readonly _languageFeaturesService: ILanguageFeaturesService, @ITextModelService private readonly _textModelService: ITextModelService, @ITerminalToolService private readonly _terminalToolService: ITerminalToolService, + @IMetricsService private readonly _metricsService: IMetricsService, ) { super() this.state = { allThreads: {}, currentThreadId: null as unknown as string } // default state @@ -266,7 +272,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // name: 'pathname_search', // params: { queryStr: 'hello', pageNumber: 0 }, // paramsStr: '{"query": "hello", "pageNumber": 0}', - // voidToolId: 'request-1', + // id: 'request-1', // } satisfies ToolRequestApproval<'pathname_search'>, { @@ -295,7 +301,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // name: 'list_dir', // params: { rootURI: URI.file('/Users/username/Documents'), pageNumber: 0 }, // paramsStr: '{"uri": "/Users/username/Documents"}', - // voidToolId: 'request-2', + // id: 'request-2', // } satisfies ToolRequestApproval<'list_dir'>, { @@ -316,7 +322,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // name: 'read_file', // params: { uri: URI.file('/Users/username/Documents/file1.txt'), pageNumber: 0 }, // paramsStr: '{"uri": "/Users/username/Documents/file1.txt"}', - // voidToolId: 'request-3', + // id: 'request-3', // } satisfies ToolRequestApproval<'read_file'>, { @@ -344,7 +350,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // name: 'search', // params: { queryStr: 'function main', pageNumber: 0 }, // paramsStr: '{"query": "function main"}', - // voidToolId: 'request-4', + // id: 'request-4', // } satisfies ToolRequestApproval<'search'>, // --- @@ -366,7 +372,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { name: 'edit', params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'Add console.log statement' }, paramsStr: '{"uri": "/Users/username/Project/main.js", "changeDescription": "Add console.log statement"}', - voidToolId: 'request-5', + id: 'request-5', } satisfies ToolRequestApproval<'edit'>, { @@ -386,7 +392,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { name: 'create_uri', params: { uri: URI.file('/Users/username/Project/new-file.js'), isFolder: false }, paramsStr: '{"uri": "/Users/username/Project/new-file.js"}', - voidToolId: 'request-6', + id: 'request-6', } satisfies ToolRequestApproval<'create_uri'>, { @@ -406,7 +412,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { name: 'delete_uri', params: { uri: URI.file('/Users/username/Project/old-file.js'), isRecursive: false, isFolder: false }, paramsStr: '{"uri": "/Users/username/Project/old-file.js", "params": ""}', - voidToolId: 'request-7', + id: 'request-7', } satisfies ToolRequestApproval<'delete_uri'>, { @@ -431,7 +437,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { name: 'terminal_command', params: { command: 'npm install', proposedTerminalId: '1', waitForCompletion: true }, paramsStr: '{"command": "npm install", "waitForCompletion": "true"}', - voidToolId: 'request-8', + id: 'request-8', } satisfies ToolRequestApproval<'terminal_command'>, @@ -493,11 +499,23 @@ class ChatThreadService extends Disposable implements IChatThreadService { return prevMessages.flatMap(m => m.role === 'user' && m.selections || []) } - private _setStreamState(threadId: string, state: Partial>) { - this.streamState[threadId] = { - ...this.streamState[threadId], - ...state + private _setStreamState(threadId: string, state: Partial>, behavior: 'set' | 'merge') { + if (state === undefined) + delete this.streamState[threadId] + + else { + if (behavior === 'merge') { + this.streamState[threadId] = { + ...this.streamState[threadId], + ...state + } + } + else if (behavior === 'set') { + this.streamState[threadId] = state + } } + + this._onDidChangeStreamState.fire({ threadId }) } @@ -506,7 +524,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { - async editUserMessageAndStreamResponse({ userMessage, chatMode, messageIdx }: { userMessage: string, chatMode: ChatMode, messageIdx: number }) { + async editUserMessageAndStreamResponse({ userMessage, messageIdx }: { userMessage: string, messageIdx: number }) { const thread = this.getCurrentThread() @@ -531,46 +549,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, true) // re-add the message and stream it - this.addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections: { prevSelns, currSelns } }) + this.addUserMessageAndStreamResponse({ userMessage, _chatSelections: { prevSelns, currSelns } }) } - private resRejOfToolAwaitingApproval: { [toolId: string]: { res: () => void, rej: () => void } } = {} - - // CAN THROW ERRORS - approveTool(toolId: string) { - const chatMode = this._settingsService.state.globalSettings.chatMode - - // if not streaming, approveToolAndStreamResponse - const threadId = this.getCurrentThread().id - const isStreaming = !!this.streamState[threadId]?.streamingToken - if (!isStreaming) { - this._approveToolAndStreamResponse_NotStreamingNow({ chatMode }) - } - else { - const resRej = this.resRejOfToolAwaitingApproval[toolId] - delete this.resRejOfToolAwaitingApproval[toolId] - resRej?.res() - } - } - rejectTool(toolId: string) { - // if not streaming, rejecttool - const threadId = this.getCurrentThread().id - const isStreaming = !!this.streamState[threadId]?.streamingToken - if (!isStreaming) { - this._rejectTool_NotStreamingNow({}) - } - else { - const resRej = this.resRejOfToolAwaitingApproval[toolId] - delete this.resRejOfToolAwaitingApproval[toolId] - resRej?.rej() - } - } - - - - private _currentModelSelectionProps = () => { // these settings should not change throughout the loop (eg anthropic breaks if you change its thinking mode and it's using tools) const featureName: FeatureName = 'Chat' @@ -579,8 +562,63 @@ class ChatThreadService extends Disposable implements IChatThreadService { return { modelSelection, modelSelectionOptions } } + + approveTool(threadId: string) { + const thread = this.state.allThreads[threadId] + if (!thread) return // should never happen + + + const lastMessage = thread.messages[thread.messages.length - 1] + if (lastMessage.role !== 'tool_request') return // should never happen + + const lastUserMsgIdx = findLastIndex(thread.messages, m => m.role === 'user') + const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' } + if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen + + const instructions = lastUserMessage.displayContent || '' + const prevSelns: StagingSelectionItem[] = this._getAllSelections() + const currSelns: StagingSelectionItem[] = [] + + const callThisToolFirst: ToolRequestApproval = lastMessage + + this._chatAgentLoop({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) + } + rejectTool(threadId: string) { + const thread = this.state.allThreads[threadId] + if (!thread) return // should never happen + + const lastMessage = thread.messages[thread.messages.length - 1] + if (lastMessage.role !== 'tool_request') return // should never happen + const { name, params, paramsStr, id } = lastMessage + + const errorMessage = this.errMsgs.rejected + this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id, content: errorMessage, result: { type: 'rejected', params: params }, }) + } + stopRunning(threadId: string) { + const thread = this.state.allThreads[threadId] + if (!thread) return // should never happen + + const llmCancelToken = this.streamState[threadId]?.streamingToken + if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) + const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + + const lastMessage = thread.messages[thread.messages.length - 1] + if (lastMessage.role === 'tool_request') { + // interrupt tool request + this.rejectTool(threadId) + } + else { + // interrupt assistant message + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) + } + this._setStreamState(threadId, {}, 'set') + } + + + private _tools = (chatMode: ChatMode) => { - const toolNames: ToolName[] | undefined = chatMode === 'chat' ? undefined + const toolNames: ToolName[] | undefined = chatMode === 'normal' ? undefined : chatMode === 'gather' ? (Object.keys(voidTools) as ToolName[]).filter(toolName => !toolNamesThatRequireApproval.has(toolName)) : chatMode === 'agent' ? Object.keys(voidTools) as ToolName[] : undefined @@ -597,20 +635,26 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - // CAN THROW ERRORS - private async _agentLoop({ threadId, tools, prevSelns, currSelns, modelSelection, modelSelectionOptions, chatMode, userMessageContent, callThisTool }: { - tools: InternalToolInfo[] | undefined, + private async _chatAgentLoop({ + threadId, + prevSelns, + currSelns, + modelSelection, + modelSelectionOptions, + userMessageContent, + callThisToolFirst, + }: { threadId: string, prevSelns: StagingSelectionItem[], currSelns: StagingSelectionItem[], modelSelection: ModelSelection | null, modelSelectionOptions: ModelSelectionOptions | undefined, - chatMode: ChatMode, userMessageContent: string, // content of LATEST user message - callThisTool?: ToolRequestApproval + callThisToolFirst?: ToolRequestApproval }) { + // define helper functions so we can tell what's going on const getLatestMessages = async () => { // recompute files in last message const selectionsStr = await chat_selectionsString(prevSelns, currSelns, this._voidFileService) // all the file CONTENTS or "selections" de-duped @@ -619,7 +663,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // replace last userMessage with userMessageFullContent (which contains all the files too) const messages_ = toLLMChatMessages(this.getCurrentThread().messages) const lastUserMsgIdx = findLastIndex(messages_, m => m.role === 'user') - if (lastUserMsgIdx === -1) throw new Error(`Void: No user message found.`) // should never be -1 + if (lastUserMsgIdx === -1) return [] // should never happen (or how did they send the message?!) // system message const workspaceFolders = this._workspaceContextService.getWorkspace().folders.map(f => f.uri.fsPath) @@ -638,9 +682,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { - const handleToolCall = async (tool: ToolCallType) => { - shouldSendAnotherMessage = true - + // returns true when the tool call is waiting for user approval + const handleToolCall = async ( + tool: ToolCallType, + opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, + ): Promise => { const toolName: ToolName = tool.name const toolParamsStr = tool.paramsStr const toolId = tool.id @@ -650,31 +696,26 @@ class ChatThreadService extends Disposable implements IChatThreadService { let toolResult: ToolResultType[typeof toolName] let toolResultStr: string - // 1. validate tool params - try { - const params = await this._toolsService.validateParams[toolName](toolParamsStr) - toolParams = params - } 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 - } - - // 2. if tool requires approval, await the approval - if (toolNamesThatRequireApproval.has(toolName)) { - const voidToolId = generateUuid() - const toolApprovalPromise = new Promise((res, rej) => { this.resRejOfToolAwaitingApproval[voidToolId] = { res, rej } }) - this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, voidToolId: voidToolId }) + if (!opts?.preapproved) { // skip this if pre-approved + // 1. validate tool params try { - await toolApprovalPromise - // accepted tool - } - catch (e) { - shouldSendAnotherMessage = false // interrupt flow by rejecting - const errorMessage = this.errMsgs.rejected - this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: errorMessage, result: { type: 'rejected', params: toolParams }, }) + const params = await this._toolsService.validateParams[toolName](toolParamsStr) + toolParams = params + } 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 } + + // 2. if tool requires approval, break from the loop, awaiting approval + const requiresApproval = true // TODO!!! + if (requiresApproval && toolNamesThatRequireApproval.has(toolName)) { + this._addMessageToThread(threadId, { role: 'tool_request', name: toolName, paramsStr: toolParamsStr, params: toolParams, id: toolId }) + return true + } + } + else { + toolParams = opts.toolParams } // 3. call the tool @@ -697,61 +738,32 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 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 true + return false }; + // above just defines helpers, below starts the actual function + const { chatMode } = this._settingsService.state.globalSettings // should not change as we loop even if user changes it, so it goes here + const tools = this._tools(chatMode) - - // CALL GIVEN TOOL before entering agent loop - const handleFirstToolCall = async (callThisTool: ToolRequestApproval) => { - const toolName: ToolName = callThisTool.name - const toolParamsStr = callThisTool.paramsStr - const toolId = callThisTool.voidToolId - - const toolParams = callThisTool.params - let toolResult: ToolResultType[typeof toolName] - let toolResultStr: string - - // 3. call the tool - try { - toolResult = await this._toolsService.callTool[toolName](toolParams as any) // ts is bad... - } catch (error) { - 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 - } - - // 4. stringify the result to give to the LLM - try { - toolResultStr = this._toolsService.stringOfResult[toolName](toolParams as any, toolResult as any) - } 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 - } - - // 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 true - } - - - this._setStreamState(threadId, { error: undefined }) // clear any previous error + // clear any previous error + set running + this._setStreamState(threadId, { isRunning: true, error: undefined }, 'set') let nMessagesSent = 0 let shouldSendAnotherMessage = true + let exitReason: 'end' | 'awaitingToolApproval' = 'end' as 'end' | 'awaitingToolApproval' - if (callThisTool) { - const keepGoing = await handleFirstToolCall(callThisTool) - if (!keepGoing) { - this._setStreamState(threadId, { streamingToken: undefined }) - return - } + // before enter loop, call tool + if (callThisToolFirst) { + await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) } + // tool use loop while (shouldSendAnotherMessage) { - shouldSendAnotherMessage = false // false by default + // false by default each iteration + shouldSendAnotherMessage = false + exitReason = 'end' + nMessagesSent += 1 let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) @@ -765,33 +777,34 @@ class ChatThreadService extends Disposable implements IChatThreadService { tools: tools, modelSelection, modelSelectionOptions, - logging: { loggingName: `Agent` }, - onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }) }, + logging: { loggingName: `Chat - ${chatMode}`, loggingExtras: { threadId, nMessagesSent, chatMode } }, + onText: ({ fullText, fullReasoning }) => { this._setStreamState(threadId, { messageSoFar: fullText, reasoningSoFar: fullReasoning }, 'merge') }, onFinalMessage: async ({ fullText, toolCalls, fullReasoning, anthropicReasoning }) => { this._addMessageToThread(threadId, { role: 'assistant', content: fullText, reasoning: fullReasoning, anthropicReasoning }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, }) // added to history, so clear messages so far + // 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') - // if no tool, finish + // call tool if there is one const tool: ToolCallType | undefined = toolCalls?.[0] - if (!tool) { - this._setStreamState(threadId, { streamingToken: undefined }) - resMessageIsDonePromise() - return - } - else { - const keepGoing = await handleToolCall(tool) - if (!keepGoing) { this._setStreamState(threadId, { streamingToken: undefined }) } - resMessageIsDonePromise() - return + if (tool) { + const awaitingUserApproval = await handleToolCall(tool) + if (awaitingUserApproval) { + exitReason = 'awaitingToolApproval' + } else { + shouldSendAnotherMessage = true + } + } + resMessageIsDonePromise() + return }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' // add assistant's message to chat history, and clear selection this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error }) + this._setStreamState(threadId, { error }, 'set') resMessageIsDonePromise() }, }) @@ -799,62 +812,31 @@ class ChatThreadService extends Disposable implements IChatThreadService { // should never happen, just for safety if (llmCancelToken === null) { this._setStreamState(threadId, { - messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined, error: { message: 'There was an unexpected error when sending your chat message.', fullError: null } - }) + }, 'set') break } - this._setStreamState(threadId, { streamingToken: llmCancelToken }) // new stream token for the new message + this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message await messageIsDonePromise } // end while + // if awaiting user approval, keep isRunning true, else end isRunning + if (exitReason === 'end') + this._setStreamState(threadId, { isRunning: undefined }, 'merge') - // TODO!!! metrics on nMessagesSent and all the file extensions sent here + // capture number of messages sent + this._metricsService.capture('Agent Loop Done', { nMessagesSent, chatMode }) } - private async _rejectTool_NotStreamingNow({ }) { - const thread = this.getCurrentThread() - const threadId = thread.id - - const lastMessage = thread.messages[thread.messages.length - 1] - if (lastMessage.role !== 'tool_request') return // should never happen - const { name, params, paramsStr, voidToolId, } = lastMessage - - const errorMessage = this.errMsgs.rejected - this._addMessageToThread(threadId, { role: 'tool', name: name, paramsStr: paramsStr, id: voidToolId, content: errorMessage, result: { type: 'rejected', params: params }, }) - - } - - // called if we stopped streaming but want to accept the tool afterwards, lets us jump back into the loop as if no interruption happened - private async _approveToolAndStreamResponse_NotStreamingNow({ chatMode }: { chatMode: ChatMode }) { - const thread = this.getCurrentThread() - const threadId = thread.id - - const lastMessage = thread.messages[thread.messages.length - 1] - if (lastMessage.role !== 'tool_request') return // should never happen - - const lastUserMsgIdx = findLastIndex(thread.messages, m => m.role === 'user') - const lastUserMessage = thread.messages[lastUserMsgIdx] as ChatMessage & { role: 'user' } - if (lastUserMsgIdx === -1 || !lastUserMessage) return // should never happen - - const instructions = lastUserMessage.displayContent || '' - const prevSelns: StagingSelectionItem[] = this._getAllSelections() - const currSelns: StagingSelectionItem[] = [] - - const tools = this._tools(chatMode) - - - const callThisTool: ToolRequestApproval = lastMessage - this._agentLoop({ callThisTool, tools, prevSelns, currSelns, threadId, chatMode, userMessageContent: instructions, ...this._currentModelSelectionProps() }) - } - async addUserMessageAndStreamResponse({ userMessage, chatMode, _chatSelections }: { userMessage: string, chatMode: ChatMode, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { + + async addUserMessageAndStreamResponse({ userMessage, _chatSelections }: { userMessage: string, _chatSelections?: { prevSelns?: StagingSelectionItem[], currSelns?: StagingSelectionItem[] } }) { const thread = this.getCurrentThread() const threadId = thread.id @@ -870,22 +852,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - const tools = this._tools(chatMode) - - this._agentLoop({ tools, prevSelns, currSelns, threadId, chatMode, userMessageContent, ...this._currentModelSelectionProps(), }) - } - - cancelStreaming(threadId: string) { - const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' - const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' - this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) - this._setStreamState(threadId, { messageSoFar: undefined, reasoningSoFar: undefined, streamingToken: undefined }) + this._chatAgentLoop({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) } dismissStreamError(threadId: string): void { - this._setStreamState(threadId, { error: undefined }) + this._setStreamState(threadId, { error: undefined }, 'merge') } @@ -901,10 +872,10 @@ class ChatThreadService extends Disposable implements IChatThreadService { const functionParensPattern = /^([^\s(]+)\([^)]*\)$/; // `functionName( args )` let target = _codespanStr // the string to search for - let codespanType: 'file' | 'function-or-class' | 'unsearchable' = 'unsearchable'; - if (target.includes('.')) { + let codespanType: 'file-or-folder' | 'function-or-class' | 'unsearchable' = 'unsearchable'; + if (target.includes('.') || target.includes('/')) { - codespanType = 'file' + codespanType = 'file-or-folder' target = _codespanStr } else if (functionOrMethodPattern.test(target)) { @@ -933,7 +904,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { .reverse() - if (codespanType === 'file') { + if (codespanType === 'file-or-folder') { const doesUriMatchTarget = (uri: URI) => uri.path.includes(target) 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 485a0937..2016ea3b 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 { LucideIcon, RotateCw } from 'lucide-react' import { Check, X, Square, Copy, Play, } from 'lucide-react' -import { getBasename, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' +import { getBasename, ListableToolItem, ToolContentsWrapper } from '../sidebar-tsx/SidebarChat.js' import { ChatMarkdownRender } from './ChatMarkdownRender.js' enum CopyButtonText { @@ -278,17 +278,27 @@ export const BlockCodeApplyWrapper = ({ const { statusIndicatorHTML, buttonsHTML } = useApplyButtonHTML({ codeStr: initValue, applyBoxId, uri }) + const accessor = useAccessor() + const commandService = accessor.get('ICommandService') - return
+ const name = uri !== 'current' ? + {getBasename(uri.fsPath)}} + isSmall={true} + showDot={false} + // TODO!!! this uri is not correct, it is not recognized as an actual file for some stupid reason + onClick={() => { commandService.executeCommand('vscode.open', uri, { preview: true }) }} + /> + : {language} + + return
{/* header */}
{statusIndicatorHTML} - {uri !== 'current' ? getBasename(uri.fsPath) : language || 'text'} + {name}
diff --git a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx index da820cdd..040f01d0 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/markdown/ChatMarkdownRender.tsx @@ -12,6 +12,7 @@ import { useAccessor } from '../util/services.js' import { ScrollType } from '../../../../../../../editor/common/editorCommon.js' import { URI } from '../../../../../../../base/common/uri.js' import { getBasename } from '../sidebar-tsx/SidebarChat.js' +import { isAbsolute } from '../../../../../../../base/common/path.js' export type ChatMessageLocation = { @@ -25,6 +26,9 @@ export const getApplyBoxId = ({ threadId, messageIdx, tokenIdx }: ApplyBoxLocati return `${threadId}-${messageIdx}-${tokenIdx}` } +function isValidUri(s: string): boolean { + return s.includes('/') && s.length > 5 && isAbsolute(s) +} const Codespan = ({ text, className, onClick }: { text: string, className?: string, onClick?: () => void }) => { @@ -122,25 +126,24 @@ const RenderToken = ({ token, inPTag, codeURI, chatMessageLocation, tokenIdx, .. if (t.type === "code") { const [firstLine, remainingContents] = getFirstLine(t.text) - const firstLineIsURI = URI.isUri(firstLine) - const contents = firstLineIsURI ? (remainingContents || '') : t.text // exclude first-line URI from contents + const firstLineIsURI = isValidUri(firstLine) + const contents = firstLineIsURI ? (remainingContents?.trimStart() || '') : t.text // exclude first-line URI from contents // figure out langauge and URI + let uri: URI | null let language: string | undefined = undefined - let uri: URI | undefined = undefined + if (firstLineIsURI) { // get lang from the uri in the first line of the markdown + uri = codeURI ?? URI.from(URI.file(firstLine)) + } + else { + uri = codeURI || null + } + if (t.lang) { // a language was provided. empty string is common so check truthy, not just undefined - uri = codeURI language = convertToVscodeLang(languageService, t.lang) // convert markdown language to language that vscode recognizes (eg markdown doesn't know bash but it does know shell) } - else { // no language provided - fallback - if (firstLineIsURI) { // get lang from the uri in the markdown - uri = codeURI ?? URI.file(firstLine) - language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined }) - } - else { // get lang from the given URI and contents - uri = codeURI - language = getLanguage(languageService, { uri: codeURI ?? null, fileContents: remainingContents ?? undefined }) - } + else { // no language provided - fallback - get lang from the uri and contents + language = getLanguage(languageService, { uri, fileContents: remainingContents ?? undefined }) } if (options.isApplyEnabled && chatMessageLocation) { diff --git a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx index bb4d72cd..0443e5c5 100644 --- a/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx +++ b/src/vs/workbench/contrib/void/browser/react/src/quick-edit-tsx/QuickEditChat.tsx @@ -96,7 +96,6 @@ export const QuickEditChat = ({ isStreaming={isStreamingRef.current} loadingIcon={loadingIcon} isDisabled={isDisabled} - className="py-2 w-full" onClickAnywhere={() => { textAreaRef.current?.focus() }} > const nameOfChatMode = { - 'chat': 'Chat', + 'normal': 'Normal', 'gather': 'Gather', 'agent': 'Agent', } const detailOfChatMode = { - 'chat': 'Normal chat', - 'gather': 'Read and search only', - 'agent': 'Full tool use', + 'normal': 'Normal chat', + 'gather': 'Discover relevant files', + 'agent': 'Edit files and use tools', } @@ -227,9 +226,8 @@ const ChatModeDropdown = ({ className }: { className: string }) => { const accessor = useAccessor() const voidSettingsService = accessor.get('IVoidSettingsService') - const voidSettingsState = useSettingsState() - const options: ChatMode[] = useMemo(() => ['chat', 'gather', 'agent'], []) + const options: ChatMode[] = useMemo(() => ['normal', 'gather', 'agent'], []) const onChangeOption = useCallback((newVal: ChatMode) => { voidSettingsService.setGlobalSetting('chatMode', newVal) @@ -304,7 +302,7 @@ export const VoidChatArea: React.FC = ({
= ({ {/* Bottom row */}
{showModelDropdown && ( -
+
-
- - +
+ {featureName === 'Chat' && } +
)} @@ -667,6 +665,7 @@ type ToolHeaderParams = { desc1: React.ReactNode; desc2?: React.ReactNode; isError?: boolean; + isRejected?: boolean; numResults?: number; children?: React.ReactNode; onClick?: () => void; @@ -683,6 +682,7 @@ const ToolHeaderWrapper = ({ isError, onClick, isOpen, + isRejected, }: ToolHeaderParams) => { const [isExpanded_, setIsExpanded] = useState(false); @@ -692,7 +692,7 @@ const ToolHeaderWrapper = ({ const isClickable = !!(isDropdown || onClick) return (
-
+
{/* header */}
)} -
+
{/* left */}
{title} @@ -723,7 +723,8 @@ const ToolHeaderWrapper = ({ {`(`}{numResults}{` result`}{numResults !== 1 ? 's' : ''}{`)`} )} - {isError && } + {isError && } + {isRejected && }
@@ -820,7 +821,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble // cancel any streams on this thread const thread = chatThreadsService.getCurrentThread() - chatThreadsService.cancelStreaming(thread.id) + chatThreadsService.stopRunning(thread.id) // update state setIsBeingEdited(false) @@ -830,7 +831,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble // stream the edit const userMessage = textAreaRefState.value; try { - await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, chatMode: 'agent', messageIdx, }) + await chatThreadsService.editUserMessageAndStreamResponse({ userMessage, messageIdx, }) } catch (e) { console.error('Error while editing message:', e) } @@ -838,7 +839,7 @@ const UserMessageComponent = ({ chatMessage, messageIdx, isLoading }: ChatBubble const onAbort = () => { const threadId = chatThreadsService.state.currentThreadId - chatThreadsService.cancelStreaming(threadId) + chatThreadsService.stopRunning(threadId) } const onKeyDown = (e: KeyboardEvent) => { @@ -1014,15 +1015,15 @@ const AssistantMessageComponent = ({ chatMessage, isLoading, messageIdx, isLast // 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 toolNameToTitle: Record = { - 'read_file': { past: 'Read file', current: 'Reading file', proposed: 'Read file' }, - 'list_dir': { past: 'Inspected folder', current: 'Inspecting folder', proposed: 'Inspect folder' }, - 'pathname_search': { past: 'Searched by file name', current: 'Searching by file name', proposed: 'Search by file name' }, - 'search': { past: 'Searched', current: 'Searching', proposed: 'Search' }, - 'create_uri': { past: 'Created file', current: 'Creating file', proposed: 'Create file' }, - 'delete_uri': { past: 'Deleted file', current: 'Deleting file', proposed: 'Delete file' }, - 'edit': { past: 'Edited file', current: 'Editing file', proposed: 'Edit file' }, - 'terminal_command': { past: 'Ran terminal command', current: 'Running terminal command', proposed: 'Run terminal command' } +const toolNameToTitle: Record = { + 'read_file': { past: 'Read file', proposed: 'Read file' }, + 'list_dir': { past: 'Inspected folder', proposed: 'Inspect folder' }, + 'pathname_search': { past: 'Searched by file name', proposed: 'Search by file name' }, + 'search': { past: 'Searched', proposed: 'Search' }, + 'create_uri': { past: 'Created file', proposed: 'Create file' }, + 'delete_uri': { past: 'Deleted file', proposed: 'Delete file' }, + 'edit': { past: 'Edited file', proposed: 'Edit file' }, + 'terminal_command': { past: 'Ran terminal command', proposed: 'Run terminal command' } } const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName] | undefined): string => { @@ -1060,24 +1061,27 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName } -const ToolRequestAcceptRejectButtons = ({ voidToolId }: { voidToolId: string }) => { +const ToolRequestAcceptRejectButtons = () => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') const metricsService = accessor.get('IMetricsService') + const onAccept = useCallback(() => { - try { - chatThreadsService.approveTool(voidToolId) + try { // this doesn't need to be wrapped in try/catch anymore + const threadId = chatThreadsService.state.currentThreadId + chatThreadsService.approveTool(threadId) metricsService.capture('Tool Request Accepted', {}) } catch (e) { console.error('Error while approving message in chat:', e) } - }, [chatThreadsService, voidToolId, metricsService]) + }, [chatThreadsService, metricsService]) const onReject = useCallback(() => { try { - chatThreadsService.rejectTool(voidToolId) + const threadId = chatThreadsService.state.currentThreadId + chatThreadsService.rejectTool(threadId) } catch (e) { console.error('Error while approving message in chat:', e) } metricsService.capture('Tool Request Rejected', {}) - }, [chatThreadsService, voidToolId, metricsService]) + }, [chatThreadsService, metricsService]) const approveButton = (
} -const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => { +export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: { name: React.ReactNode, onClick?: () => void, isSmall?: boolean, className?: string, showDot?: boolean }) => { return
{ commandService.executeCommand('vscode.open', uri, { preview: true }) }} /> -
-
- -
+
+
} @@ -1183,12 +1185,10 @@ const TerminalToolChildren = ({ command, terminalId, result, resolveReason }: { className='w-full overflow-auto py-1' onClick={() => terminalToolsService.openTerminal(terminalId)} /> -
-
- {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} - {result} - {resultStr} -
+
+ {resolveReason.type === 'bgtask' ? 'Result so far:\n' : null} + {result} + {resultStr}
} @@ -1203,7 +1203,7 @@ const ReasoningWrapper = ({ isDoneReasoning, isStreaming, children }: { isDoneRe if (!isWriting) setIsOpen(isWriting) // if just finished reasoning, close }, [isWriting]) return : ''} isOpen={isOpen}> - +
{children}
@@ -1396,7 +1396,8 @@ const toolNameToComponent: { [T in ToolName]: { const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { const { params } = toolMessage.result @@ -1434,12 +1435,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage }) => { const accessor = useAccessor() const commandService = accessor.get('ICommandService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { const { params } = toolMessage.result @@ -1480,12 +1482,13 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ({ toolMessage, messageIdx }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success' || toolMessage.result.type === 'rejected') { const { params } = toolMessage.result @@ -1540,12 +1543,13 @@ const toolNameToComponent: { [T in ToolName]: { const accessor = useAccessor() const commandService = accessor.get('ICommandService') const terminalToolsService = accessor.get('ITerminalToolService') - const title = toolNameToTitle[toolMessage.name].past + const title = toolMessage.result.type === 'success' ? toolNameToTitle[toolMessage.name].past : toolNameToTitle[toolMessage.name].proposed const desc1 = toolNameToDesc(toolMessage.name, toolMessage.result.params) const icon = null const isError = toolMessage.result.type === 'error' - const componentParams: ToolHeaderParams = { title, desc1, isError, icon } + const isRejected = toolMessage.result.type === 'rejected' + const componentParams: ToolHeaderParams = { title, desc1, isError, icon, isRejected } if (toolMessage.result.type === 'success') { const { command } = toolMessage.result.params @@ -1605,11 +1609,11 @@ const ChatBubble = ({ chatMessage, isLoading, messageIdx, isLast }: ChatBubblePr } else if (role === 'tool_request') { const ToolRequestWrapper = toolNameToComponent[chatMessage.name].requestWrapper as React.FC<{ toolRequest: any }> // ts isnt smart enough... - // if (!isLast) return null + if (!isLast) return null if (!ToolRequestWrapper) return null return <> - + } else if (role === 'tool') { @@ -1838,7 +1842,7 @@ export const SidebarChat = () => { // stream state const currThreadStreamState = useChatThreadsStreamState(chatThreadsState.currentThreadId) - const isStreaming = !!currThreadStreamState?.streamingToken + const isRunning = !!currThreadStreamState?.isRunning const latestError = currThreadStreamState?.error const messageSoFar = currThreadStreamState?.messageSoFar const reasoningSoFar = currThreadStreamState?.reasoningSoFar @@ -1861,7 +1865,7 @@ export const SidebarChat = () => { const onSubmit = useCallback(async () => { if (isDisabled) return - if (isStreaming) return + if (isRunning) return // update state chatThreadsService.closeStagingSelectionsInCurrentThread() // close all selections @@ -1871,10 +1875,8 @@ export const SidebarChat = () => { // getModelCapabilities() // TODO!!! check if can go into agent mode - const chatMode = settingsState.globalSettings.chatMode - try { - await chatThreadsService.addUserMessageAndStreamResponse({ userMessage, chatMode }) + await chatThreadsService.addUserMessageAndStreamResponse({ userMessage }) } catch (e) { console.error('Error while sending message in chat:', e) } @@ -1883,11 +1885,11 @@ export const SidebarChat = () => { textAreaFnsRef.current?.setValue('') textAreaRef.current?.focus() // focus input after submit - }, [chatThreadsService, isDisabled, isStreaming, textAreaRef, textAreaFnsRef, setSelections]) + }, [chatThreadsService, isDisabled, isRunning, textAreaRef, textAreaFnsRef, setSelections, settingsState]) const onAbort = () => { const threadId = currentThread.id - chatThreadsService.cancelStreaming(threadId) + chatThreadsService.stopRunning(threadId) } // const [_test_messages, _set_test_messages] = useState([]) @@ -1910,7 +1912,7 @@ export const SidebarChat = () => { }, [previousMessages, currentThread, numMessages]) const streamingChatIdx = previousMessagesHTML.length - const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isStreaming) ? + const currStreamingMessageHTML = !!(reasoningSoFar || messageSoFar || isRunning) ? { reasoning: reasoningSoFar ?? '', anthropicReasoning: null, }} - isLoading={isStreaming} + isLoading={isRunning} isLast={true} /> : null @@ -1970,10 +1972,10 @@ export const SidebarChat = () => { const onKeyDown = useCallback((e: KeyboardEvent) => { if (e.key === 'Enter' && !e.shiftKey) { onSubmit() - } else if (e.key === 'Escape' && isStreaming) { + } else if (e.key === 'Escape' && isRunning) { onAbort() } - }, [onSubmit, onAbort, isStreaming]) + }, [onSubmit, onAbort, isRunning]) const inputForm =
0 ? 'absolute bottom-0' : ''}`}> @@ -1982,7 +1984,7 @@ export const SidebarChat = () => { divRef={chatAreaRef} onSubmit={onSubmit} onAbort={onAbort} - isStreaming={isStreaming} + isStreaming={isRunning} isDisabled={isDisabled} showSelections={true} showProspectiveSelections={previousMessagesHTML.length === 0} @@ -1991,7 +1993,8 @@ export const SidebarChat = () => { onClickAnywhere={() => { textAreaRef.current?.focus() }} > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`} + // className={`${previousMessages.length > 0 ? 'min-h-[9px]' : 'min-h-[81px]'} px-0.5`} + className={`min-h-[81px] px-0.5 py-0.5`} placeholder={`${keybindingString ? `${keybindingString} to select. ` : ''}Enter instructions...`} onChangeText={onChangeText} onKeyDown={onKeyDown} diff --git a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts index 893b4dc1..4667b62e 100644 --- a/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/chatThreadServiceTypes.ts @@ -21,7 +21,7 @@ export type ToolRequestApproval = { name: T; // internal use params: ToolCallParams[T]; // internal use paramsStr: string; // internal use - this is what the LLM outputted, not necessarily JSON.stringify(params) - voidToolId: string; // internal id Void uses + id: string; // proposed tool's id } // WARNING: changing this format is a big deal!!!!!! need to migrate old format to new format on users' computers so people don't get errors. diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 067fc649..ddc9d35f 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -8,6 +8,7 @@ import { URI } from '../../../../../base/common/uri.js'; import { os } from '../helpers/systemInfo.js'; import { IVoidFileService } from '../voidFileService.js'; import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThreadServiceTypes.js'; +import { ChatMode } from '../voidSettingsTypes.js'; // this is just for ease of readability @@ -21,8 +22,8 @@ Do NOT output the whole file here if possible, and try to write as LITTLE code a -export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: 'agent' | 'gather' | 'chat') => `\ -You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase'}. +export const chat_systemMessage = (workspaces: string[], runningTerminalIds: string[], mode: ChatMode) => `\ +You are an expert coding ${mode === 'agent' ? 'agent' : 'assistant'} created by Void. Your job is to help the user ${mode === 'agent' ? 'develop, run, and make changes to their project' : 'search and understand their codebase by providing specific references to files and content'}. You will be given instructions to follow from the user, \`INSTRUCTIONS\`. You may also be given a list of files that the user has specifically selected, \`SELECTIONS\`. Please assist the user with their query${mode === 'agent' ? `, bringing the task to completion (make all necessary changes, and do not be lazy)` : ''}. The user's query is never invalid. @@ -56,6 +57,7 @@ If you think it's appropriate to suggest an edit to a file, then you must descri Misc: - Do not make things up. +- Do not be lazy. - Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ ` diff --git a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts index b27ea20b..9ecb680e 100644 --- a/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts +++ b/src/vs/workbench/contrib/void/common/sendLLMMessageTypes.ts @@ -81,7 +81,7 @@ export type ServiceSendLLMMessageParams = { onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; - logging: { loggingName: string, }; + logging: { loggingName: string, loggingExtras?: { [k: string]: any } }; modelSelection: ModelSelection | null; modelSelectionOptions: ModelSelectionOptions | undefined; } & SendLLMType; @@ -91,7 +91,7 @@ export type SendLLMMessageParams = { onText: OnText; onFinalMessage: OnFinalMessage; onError: OnError; - logging: { loggingName: string, }; + logging: { loggingName: string, loggingExtras?: { [k: string]: any } }; abortRef: AbortRef; aiInstructions: string; diff --git a/src/vs/workbench/contrib/void/common/voidSettingsService.ts b/src/vs/workbench/contrib/void/common/voidSettingsService.ts index 10eee7b2..e631a8aa 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsService.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsService.ts @@ -99,7 +99,7 @@ const _updatedModelsAfterDefaultModelsChange = (defaultModelNames: string[], opt export const modelFilterOfFeatureName: { [featureName in FeatureName]: { filter: (o: ModelSelection, opts: { chatMode: ChatMode }) => boolean; emptyMessage: null | { message: string, priority: 'always' | 'fallback' } } } = { 'Autocomplete': { filter: (o) => getModelCapabilities(o.providerName, o.modelName).supportsFIM, emptyMessage: { message: 'No models support FIM', priority: 'always' } }, - 'Chat': { filter: (o, { chatMode }) => chatMode === 'chat' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } }, + 'Chat': { filter: (o, { chatMode }) => chatMode === 'normal' ? true : !!getModelCapabilities(o.providerName, o.modelName).supportsTools, emptyMessage: { message: 'No models support tool use', priority: 'fallback' } }, 'Ctrl+K': { filter: o => true, emptyMessage: null, }, 'Apply': { filter: o => true, emptyMessage: null, }, } diff --git a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts index b0c2c618..6668610f 100644 --- a/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts +++ b/src/vs/workbench/contrib/void/common/voidSettingsTypes.ts @@ -378,7 +378,7 @@ export const isFeatureNameDisabled = (featureName: FeatureName, settingsState: V -export type ChatMode = 'agent' | 'gather' | 'chat' +export type ChatMode = 'agent' | 'gather' | 'normal' export type GlobalSettings = { diff --git a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts index 1e17d97b..25f0e19e 100644 --- a/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts +++ b/src/vs/workbench/contrib/void/electron-main/llmMessage/sendLLMMessage.ts @@ -17,7 +17,7 @@ export const sendLLMMessage = ({ onFinalMessage: onFinalMessage_, onError: onError_, abortRef: abortRef_, - logging: { loggingName }, + logging: { loggingName, loggingExtras }, settingsOfProvider, modelSelection, modelSelectionOptions, @@ -48,6 +48,7 @@ export const sendLLMMessage = ({ suffixLength: messages_.suffix.length, } : {}, + ...loggingExtras, ...extras, }) }