diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index 3f2aaad3..2455bb44 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -11,13 +11,13 @@ import { IStorageService, StorageScope, StorageTarget } from '../../../../platfo import { URI } from '../../../../base/common/uri.js'; import { Emitter, Event } from '../../../../base/common/event.js'; import { ILLMMessageService } from '../common/sendLLMMessageService.js'; -import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString } from '../common/prompt/prompts.js'; +import { chat_userMessageContent, chat_systemMessage, chat_lastUserMessageWithFilesAdded, chat_selectionsString, voidTools } from '../common/prompt/prompts.js'; import { getErrorMessage, LLMChatMessage, ToolCallType } from '../common/sendLLMMessageTypes.js'; import { IWorkspaceContextService } from '../../../../platform/workspace/common/workspace.js'; import { generateUuid } from '../../../../base/common/uuid.js'; import { ChatMode, FeatureName, ModelSelection, ModelSelectionOptions } from '../common/voidSettingsTypes.js'; import { IVoidSettingsService } from '../common/voidSettingsService.js'; -import { ToolName, ToolCallParams, ToolResultType, InternalToolInfo, voidTools, toolNamesThatRequireApproval } from '../common/toolsServiceTypes.js'; +import { ToolName, ToolCallParams, ToolResultType, toolNamesThatRequireApproval, InternalToolInfo } from '../common/toolsServiceTypes.js'; import { IToolsService } from './toolsService.js'; import { CancellationToken } from '../../../../base/common/cancellation.js'; import { ILanguageFeaturesService } from '../../../../editor/common/services/languageFeatures.js'; @@ -109,11 +109,11 @@ export type ThreadsState = { export type IsRunningType = undefined | 'message' | 'tool' | 'awaiting_user' export type ThreadStreamState = { [threadId: string]: undefined | { - // state related + // state related to streaming (not just when streaming) 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 + // streaming related - when streaming message streamingToken?: string; messageSoFar?: string; reasoningSoFar?: string; @@ -426,7 +426,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { result: { type: 'success', params: { uri: URI.file('/Users/username/Project/main.js'), changeDescription: 'I think we should do this:\n```typescript\n//Add console.log statement\n for i in ...\n\t\tdo:\nabc\n```' }, - value: {} + value: Promise.resolve() }, } satisfies ToolMessage<'edit'>, { @@ -645,14 +645,12 @@ class ChatThreadService extends Disposable implements IChatThreadService { const callThisToolFirst: ToolRequestApproval = lastMessage - this._chatAgentLoop({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) + this._runChatAgent({ callThisToolFirst, prevSelns, currSelns, threadId, userMessageContent: instructions, ...this._currentModelSelectionProps() }) } rejectTool(threadId: string) { 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,21 +663,24 @@ class ChatThreadService extends Disposable implements IChatThreadService { const thread = this.state.allThreads[threadId] if (!thread) return // should never happen - 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 + const isRunning = this.streamState[threadId]?.isRunning + // reject the tool for the user + if (isRunning === 'awaiting_user') { this.rejectTool(threadId) } - else { - // interrupt assistant message + // interrupt the tool + else if (isRunning === 'tool') { + this._currentlyRunningToolInterruptor[threadId]?.() + } + // interrupt assistant message + else if (isRunning === 'message') { + // abort the stream first so it doesn't change any state + const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' + const reasoningSoFar = this.streamState[threadId]?.reasoningSoFar ?? '' + + const llmCancelToken = this.streamState[threadId]?.streamingToken + if (llmCancelToken !== undefined) { this._llmMessageService.abort(llmCancelToken) } + this._addMessageToThread(threadId, { role: 'assistant', content: messageSoFar, reasoning: reasoningSoFar, anthropicReasoning: null }) } @@ -706,9 +707,9 @@ class ChatThreadService extends Disposable implements IChatThreadService { } - private readonly _cancelToolOfThreadId: { [threadId: string]: (() => void) | undefined } = {} + private readonly _currentlyRunningToolInterruptor: { [threadId: string]: (() => void) | undefined } = {} - private async _chatAgentLoop({ + private async _runChatAgent({ threadId, prevSelns, currSelns, @@ -761,7 +762,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const handleToolCall = async ( tool: ToolCallType, opts?: { preapproved: true, toolParams: ToolCallParams[ToolName] }, - ): Promise<{ awaitingUserApproval?: boolean, canceled?: boolean }> => { + ): Promise<{ awaitingUserApproval?: boolean, interrupted?: boolean }> => { const toolName: ToolName = tool.name const toolParamsStr = tool.paramsStr const toolId = tool.id @@ -799,17 +800,22 @@ class ChatThreadService extends Disposable implements IChatThreadService { // 3. call the tool this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - let canceled = false + let interrupted = false try { - 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... + const { result, interruptTool } = await this._toolsService.callTool[toolName](toolParams as any) + this._currentlyRunningToolInterruptor[threadId] = () => { + interrupted = true; + interruptTool?.(); + delete this._currentlyRunningToolInterruptor[threadId]; + } + toolResult = await result // ts is bad... await is needed } catch (error) { - if (canceled) return { canceled: true } + if (interrupted) { + // ideally this should have same implementation as abort - addMessage should get called in stopRunning + this._addMessageToThread(threadId, { role: 'tool', name: toolName, paramsStr: toolParamsStr, id: toolId, content: this.errMsgs.rejected, result: { type: 'rejected', params: toolParams }, }) + return { interrupted: 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 {} @@ -843,25 +849,22 @@ class ChatThreadService extends Disposable implements IChatThreadService { // before enter loop, call tool if (callThisToolFirst) { - this._setStreamState(threadId, { isRunning: 'tool' }, 'merge') - const { canceled } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) - if (canceled) return + const { interrupted } = await handleToolCall(callThisToolFirst, { preapproved: true, toolParams: callThisToolFirst.params }) + if (interrupted) return } // tool use loop while (shouldSendAnotherMessage) { - this._setStreamState(threadId, { isRunning: 'message' }, 'merge') - // false by default each iteration shouldSendAnotherMessage = false isRunningWhenEnd = undefined - nMessagesSent += 1 - let resMessageIsDonePromise: () => void // resolves when user approves this tool use (or if tool doesn't require approval) - const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) + let resMessageIsDonePromise: (toolCalls?: ToolCallType[] | undefined) => void // resolves when user approves this tool use (or if tool doesn't require approval) + const messageIsDonePromise = new Promise((res, rej) => { resMessageIsDonePromise = res }) // send llm message + this._setStreamState(threadId, { isRunning: 'message' }, 'merge') const messages = await getLatestMessages() const llmCancelToken = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', @@ -872,23 +875,11 @@ class ChatThreadService extends Disposable implements IChatThreadService { 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 }) // 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') - - // call tool if there is one - const tool: ToolCallType | undefined = toolCalls?.[0] - if (tool) { - const { awaitingUserApproval } = await handleToolCall(tool) // things happen correctly if canceled is true here, because canceled calls onAbort - if (awaitingUserApproval) { - isRunningWhenEnd = 'awaiting_user' - } else { - shouldSendAnotherMessage = true - } - - } - resMessageIsDonePromise() + // resolve with tool calls + resMessageIsDonePromise(toolCalls) }, onError: (error) => { const messageSoFar = this.streamState[threadId]?.messageSoFar ?? '' @@ -913,11 +904,28 @@ class ChatThreadService extends Disposable implements IChatThreadService { }, 'set') break } - this._setStreamState(threadId, { streamingToken: llmCancelToken }, 'merge') // new stream token for the new message - - await messageIsDonePromise + const toolCalls = await messageIsDonePromise // wait for message to complete if (aborted) { return } + this._setStreamState(threadId, { streamingToken: undefined }, 'merge') // streaming message is done + + // call tool if there is one + const tool: ToolCallType | undefined = toolCalls?.[0] + if (tool) { + const { awaitingUserApproval, interrupted } = await handleToolCall(tool) + + // stop if interrupted. we don't have to do this for llmMessage because we have a stream token for it and onAbort gets called, but we don't have the equivalent for tools. + // just detect tool interruption which is the same as chat interruption right now + if (interrupted) { return } + + if (awaitingUserApproval) { + isRunningWhenEnd = 'awaiting_user' + } + else { + shouldSendAnotherMessage = true + } + } + } // end while @@ -940,9 +948,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { // if the current thread is already streaming, stop it (this simply resolves the promise to free up space) const llmCancelToken = this.streamState[threadId]?.streamingToken - if (llmCancelToken) { - if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) - } + if (llmCancelToken !== undefined) this._llmMessageService.abort(llmCancelToken) // selections in all past chats, then in current chat (can have many duplicates here) const prevSelns: StagingSelectionItem[] = _chatSelections?.prevSelns ?? this._getAllSelections(threadId) @@ -955,7 +961,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { const userHistoryElt: ChatMessage = { role: 'user', content: userMessageContent, displayContent: instructions, selections: currSelns, state: defaultMessageState } this._addMessageToThread(threadId, userHistoryElt) - this._chatAgentLoop({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) + this._runChatAgent({ prevSelns, currSelns, threadId, userMessageContent, ...this._currentModelSelectionProps(), }) } dismissStreamError(threadId: string): void { diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index 54883d08..dba6870a 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -722,7 +722,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - private _addToHistory(uri: URI, opts?: { onUndo?: () => void }) { + private _addToHistory(uri: URI, opts?: { onWillUndo?: () => void }) { const getCurrentSnapshot = (): HistorySnapshot => { @@ -807,7 +807,7 @@ class EditCodeService extends Disposable implements IEditCodeService { resource: uri, label: 'Void Changes', code: 'undoredo.editCode', - undo: () => { restoreDiffAreas(beforeSnapshot); opts?.onUndo?.() }, + undo: () => { opts?.onWillUndo?.(); restoreDiffAreas(beforeSnapshot); }, redo: () => { if (afterSnapshot) restoreDiffAreas(afterSnapshot) } } this._undoRedoService.pushElement(elt) @@ -1163,7 +1163,7 @@ class EditCodeService extends Disposable implements IEditCodeService { - // the applyDonePromise this returns can throw an error (reject) + // the applyDonePromise this returns can reject, and should be caught with .catch public async startApplying(opts: StartApplyingOpts): Promise<[URI, Promise] | null> { let res: [DiffZone, Promise] | undefined = undefined @@ -1212,14 +1212,14 @@ class EditCodeService extends Disposable implements IEditCodeService { uri, startBehavior, streamRequestIdRef, - onUndo, linkedCtrlKZone, + onWillUndo, }: { uri: URI, startBehavior: 'accept-conflicts' | 'reject-conflicts' | 'keep-conflicts', streamRequestIdRef: { current: string | null }, linkedCtrlKZone: CtrlKZone | null, - onUndo: () => void, + onWillUndo: () => void, }) { const { model } = this._voidModelService.getModel(uri) if (!model) return @@ -1235,8 +1235,9 @@ class EditCodeService extends Disposable implements IEditCodeService { const originalFileStr = model.getValue(EndOfLinePreference.LF) let originalCode = model.getValueInRange(range, EndOfLinePreference.LF) + // add to history as a checkpoint, before we start modifying - const { onFinishEdit } = this._addToHistory(uri, { onUndo }) + const { onFinishEdit } = this._addToHistory(uri, { onWillUndo }) // clear diffZones so no conflict if (startBehavior === 'keep-conflicts') { @@ -1328,21 +1329,12 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error(`Void: diff.type not recognized on: ${from}`) } - console.log('q1', this.diffAreaOfId) await this._voidModelService.initializeModel(uri) - console.log('q2', this.diffAreaOfId) const { model } = this._voidModelService.getModel(uri) if (!model) return - console.log('q3', this.diffAreaOfId) - - // promise that resolves when the apply is done - let resApplyPromise: () => void - let rejApplyPromise: (e: any) => void - const applyPromise = new Promise((res_, rej_) => { resApplyPromise = res_; rejApplyPromise = rej_ }) let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId - // build messages const quickEditFIMTags = defaultQuickEditFimTags // TODO can eventually let users customize modelFimTags const originalFileCode = model.getValue(EndOfLinePreference.LF) @@ -1357,13 +1349,9 @@ class EditCodeService extends Disposable implements IEditCodeService { ] } else if (from === 'QuickEdit') { - console.log('aaa') if (!ctrlKZoneIfQuickEdit) return - console.log('bbb', ctrlKZoneIfQuickEdit) const { _mountInfo } = ctrlKZoneIfQuickEdit - console.log('ccc', _mountInfo) const instructions = _mountInfo?.textAreaRef.current?.value ?? '' - console.log('ddd', instructions) const startLine = startRange === 'fullFile' ? 1 : startRange[0] const endLine = startRange === 'fullFile' ? model.getLineCount() : startRange[1] @@ -1378,17 +1366,22 @@ class EditCodeService extends Disposable implements IEditCodeService { else { throw new Error(`featureName ${from} is invalid`) } + // start diffzone const res = this._startStreamingDiffZone({ uri, streamRequestIdRef, startBehavior: opts.startBehavior, - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyPromise(new Error('Edit was interrupted by pressing undo.')) }, linkedCtrlKZone: ctrlKZoneIfQuickEdit, + onWillUndo: () => { + if (streamRequestIdRef.current) { + this._llmMessageService.abort(streamRequestIdRef.current) + } + }, + }) if (!res) return - const { diffZone, onFinishEdit } = res - + const { diffZone, onFinishEdit, } = res // helpers @@ -1408,6 +1401,14 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit() } + // throws + const onError = (e: { message: string; fullError: Error | null; }) => { + this._notifyError(e) + onDone() + this._undoHistory(uri) + throw e.fullError + } + const extractText = (fullText: string, recentlyAddedTextLen: number) => { if (from === 'QuickEdit') { return extractCodeFromFIM({ text: fullText, recentlyAddedTextLen, midTag: quickEditFIMTags.midTag }) @@ -1418,7 +1419,6 @@ class EditCodeService extends Disposable implements IEditCodeService { throw new Error('Void 1') } - // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) @@ -1428,7 +1428,8 @@ class EditCodeService extends Disposable implements IEditCodeService { const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined - const writeover = async () => { + // allowed to throw errors - this is called inside a promise that handles everything + const runWriteover = async () => { let shouldSendAnotherMessage = true while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false @@ -1440,6 +1441,8 @@ class EditCodeService extends Disposable implements IEditCodeService { let fullTextSoFar = '' // so far (INCLUDING ignored suffix) let prevIgnoredSuffix = '' let aborted = false + let weAreAborting = false + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', @@ -1476,31 +1479,27 @@ class EditCodeService extends Disposable implements IEditCodeService { resMessageDonePromise() }, onError: (e) => { - this._notifyError(e) - onDone() - this._undoHistory(uri) - resMessageDonePromise() + onError(e) }, onAbort: () => { + if (weAreAborting) return // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) - resMessageDonePromise() aborted = true + resMessageDonePromise() }, }) // should never happen, just for safety if (streamRequestIdRef.current === null) { return } await messageDonePromise - if (aborted) { return } + if (aborted) { + throw new Error(`Edit was interrupted by the user.`) + } } // end while } // end writeover - writeover().then(() => { - resApplyPromise() - // this._noLongerNeedModelReference(uri) - }).catch((e) => rejApplyPromise(e)) - - return [diffZone, applyPromise] + const applyDonePromise = runWriteover() + return [diffZone, applyDonePromise] } @@ -1523,10 +1522,6 @@ class EditCodeService extends Disposable implements IEditCodeService { const { model } = this._voidModelService.getModel(uri) if (!model) return - // promise that resolves when the apply is done - let resApplyDonePromise: () => void - let rejApplyDonePromise: (e: any) => void - const applyDonePromise = new Promise((res_, rej_) => { resApplyDonePromise = res_; rejApplyDonePromise = rej_ }) let streamRequestIdRef: { current: string | null } = { current: null } // can use this as a proxy to set the diffArea's stream state requestId @@ -1544,7 +1539,11 @@ class EditCodeService extends Disposable implements IEditCodeService { streamRequestIdRef, startBehavior: opts.startBehavior, linkedCtrlKZone: null, - onUndo: () => { if (diffZone._streamState.isStreaming) rejApplyDonePromise(new Error('Edit was interrupted by user pressing undo.')) }, + onWillUndo: () => { + if (streamRequestIdRef.current) { + this._llmMessageService.abort(streamRequestIdRef.current) // triggers onAbort() + } + }, }) if (!res) return const { diffZone, onFinishEdit } = res @@ -1573,7 +1572,7 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const errMsgOfInvalidStr = (str: string & ReturnType, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => { + const errContentOfInvalidStr = (str: string & ReturnType, blockOrig: string, blockNum: number, blocks: ExtractedSearchReplaceBlock[]) => { const descStr = str === `Not found` ? `The most recent ORIGINAL code could not be found in the file, so you were interrupted. The text in ORIGINAL must EXACTLY match lines of code. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` @@ -1604,6 +1603,13 @@ class EditCodeService extends Disposable implements IEditCodeService { onFinishEdit() } + const onError = (e: { message: string; fullError: Error | null; }) => { + this._notifyError(e) + onDone() + this._undoHistory(uri) + throw e.fullError // throw error h + } + // refresh now in case onText takes a while to get 1st message this._refreshStylesAndDiffsInURI(uri) @@ -1614,12 +1620,14 @@ class EditCodeService extends Disposable implements IEditCodeService { const addedTrackingZoneOfBlockNum: TrackingZone[] = [] diffZone._streamState.line = 1 - const featureName: FeatureName = 'Apply' const modelSelection = this._settingsService.state.modelSelectionOfFeature[featureName] const modelSelectionOptions = modelSelection ? this._settingsService.state.optionsOfModelSelection[modelSelection.providerName]?.[modelSelection.modelName] : undefined - const retryLoop = async () => { + const N_RETRIES = 5 + + // allowed to throw errors - this is called inside a promise that handles everything + const runSearchReplace = async () => { // this generates >>>>>>> ORIGINAL <<<<<<< REPLACE blocks and and simultaneously applies it let shouldSendAnotherMessage = true let nMessagesSent = 0 @@ -1629,10 +1637,12 @@ class EditCodeService extends Disposable implements IEditCodeService { while (shouldSendAnotherMessage) { shouldSendAnotherMessage = false nMessagesSent += 1 - if (nMessagesSent >= 5) { - this._notifyError({ message: 'Tried to Fast Apply 5 times but failed. Please try again with a smarter model or disable Fast Apply.', fullError: null }) - onDone() - this._undoHistory(uri) + if (nMessagesSent >= N_RETRIES) { + const e = { + message: `Tried to Fast Apply ${N_RETRIES} times but failed. This may be related to model intelligence, or it may an edit that's too complex. Please retry or disable Fast Apply.`, + fullError: null + } + onError(e) break } @@ -1693,7 +1703,7 @@ class EditCodeService extends Disposable implements IEditCodeService { console.log('fullText', { fullText }) console.log('error:', originalBounds) console.log('block.orig:', block.orig) - const content = errMsgOfInvalidStr(originalBounds, block.orig, blockNum, blocks) + const content = errContentOfInvalidStr(originalBounds, block.orig, blockNum, blocks) messages.push( { role: 'assistant', content: fullText, anthropicReasoning: null }, // latest output { role: 'user', content: content } // user explanation of what's wrong @@ -1769,8 +1779,6 @@ class EditCodeService extends Disposable implements IEditCodeService { // diffZone._streamState.line = currentEndLine diffZone._streamState.line = latestStreamLocationMutable.line - - } // end for this._refreshStylesAndDiffsInURI(uri) @@ -1822,41 +1830,45 @@ class EditCodeService extends Disposable implements IEditCodeService { resMessageDonePromise() }, onError: (e) => { - this._notifyError(e) - onDone() - this._undoHistory(uri) - resMessageDonePromise() + onError(e) }, onAbort: () => { if (weAreAborting) return // stop the loop to free up the promise, but don't modify state (already handled by whatever stopped it) - resMessageDonePromise() aborted = true + resMessageDonePromise() }, }) // should never happen, just for safety if (streamRequestIdRef.current === null) { break } - console.log('awaiting...') await messageDonePromise - console.log('done awaiting, aborted=', aborted) - if (aborted) { return } - + if (aborted) { + throw new Error(`Edit was interrupted by the user.`) + } } // end while } // end retryLoop - retryLoop().then(() => { - console.log('resolving Apply Done') - resApplyDonePromise() - // this._noLongerNeedModelReference(uri) - }).catch((e) => rejApplyDonePromise(e)) - + const applyDonePromise = runSearchReplace() return [diffZone, applyDonePromise] } + _undoHistory(uri: URI) { + this._undoRedoService.undo(uri) + } + + + + isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { + const ctrlKZone = this.diffAreaOfId[diffareaid] + if (!ctrlKZone) return false + if (ctrlKZone.type !== 'CtrlKZone') return false + return !!ctrlKZone._linkedStreamingDiffZone + } + private _stopIfStreaming(diffZone: DiffZone) { const uri = diffZone._URI @@ -1870,31 +1882,6 @@ class EditCodeService extends Disposable implements IEditCodeService { this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) } - _undoHistory(uri: URI) { - this._undoRedoService.undo(uri) - } - - - - - - _interruptSingleDiffZoneStreaming({ diffareaid }: { diffareaid: number }) { - const diffZone = this.diffAreaOfId[diffareaid] - if (diffZone?.type !== 'DiffZone') return - if (!diffZone._streamState.isStreaming) return - - this._stopIfStreaming(diffZone) - this._undoHistory(diffZone._URI) - } - - - isCtrlKZoneStreaming({ diffareaid }: { diffareaid: number }) { - const ctrlKZone = this.diffAreaOfId[diffareaid] - if (!ctrlKZone) return false - if (ctrlKZone.type !== 'CtrlKZone') return false - return !!ctrlKZone._linkedStreamingDiffZone - } - // diffareaid of the ctrlKZone (even though the stream state is dictated by the linked diffZone) interruptCtrlKStreaming({ diffareaid }: { diffareaid: number }) { @@ -1906,7 +1893,8 @@ class EditCodeService extends Disposable implements IEditCodeService { if (!linkedStreamingDiffZone) return if (linkedStreamingDiffZone.type !== 'DiffZone') return - this._interruptSingleDiffZoneStreaming({ diffareaid: linkedStreamingDiffZone.diffareaid }) + this._stopIfStreaming(linkedStreamingDiffZone) + this._undoHistory(linkedStreamingDiffZone._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 bff5576a..0927ab5f 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 @@ -159,15 +159,13 @@ export const useApplyButtonHTML = ({ codeStr, applyBoxId, uri }: { codeStr: stri const onClickSubmit = useCallback(async () => { if (isDisabled) return if (getStreamState() === 'streaming') return - const [newApplyingUri, _] = await editCodeService.startApplying({ + const [newApplyingUri, applyDonePromise] = await editCodeService.startApplying({ from: 'ClickApply', applyStr: codeStr, uri: uri, startBehavior: 'keep-conflicts', }) ?? [] - if (!newApplyingUri) console.log('NOT new applying') - applyingURIOfApplyBoxIdRef.current[applyBoxId] = newApplyingUri ?? undefined rerender(c => c + 1) 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 205ef3a9..d6ed8605 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 @@ -63,7 +63,7 @@ export const QuickEditChat = ({ if (isStreamingRef.current) return textAreaFnsRef.current?.disable() - editCodeService.startApplying({ + const res = editCodeService.startApplying({ from: 'QuickEdit', diffareaid, startBehavior: 'keep-conflicts', 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 dc776649..99d6bf62 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 @@ -1040,7 +1040,7 @@ const ProseWrapper = ({ children }: { children: React.ReactNode }) => { {children} } -const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, }) => { +const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunning }: { chatMessage: ChatMessage & { role: 'assistant' }, messageIdx: number, isCommitted: boolean, isLast: boolean, chatIsRunning: IsRunningType }) => { const accessor = useAccessor() const chatThreadsService = accessor.get('IChatThreadService') @@ -1058,7 +1058,7 @@ const AssistantMessageComponent = ({ chatMessage, isCommitted, messageIdx, isLas } const isEmpty = !chatMessage.content && !chatMessage.reasoning - const isLastAndLoading = !isCommitted && isLast + const isLastAndLoading = !isCommitted && isLast && (chatIsRunning === 'message' || chatIsRunning === 'awaiting_user') if (isEmpty && !isLastAndLoading) return null return <> @@ -1770,6 +1770,7 @@ const ChatBubble = ({ chatMessage, isCommitted, messageIdx, isLast, chatIsRunnin chatMessage={chatMessage} messageIdx={messageIdx} isCommitted={isCommitted} + chatIsRunning={chatIsRunning} isLast={isLast} /> } @@ -1893,7 +1894,7 @@ export const SidebarChat = () => { const previousMessagesHTML = useMemo(() => { const threadId = currentThread.id return previousMessages.map((message, i) => { - const isLast = i === numMessages - 1 + const isLast = i === numMessages - 1 && (isRunning === 'tool' || isRunning === 'awaiting_user') return { isLast={isLast} threadId={threadId} /> - } - ) + }) }, [previousMessages, isRunning, currentThread, numMessages]) const threadId = currentThread.id 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 407305cd..b052ae66 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 @@ -48,16 +48,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { // changes if the user clicks left/right or if the user goes on a uri with changes const [currUriIdx, setUriIdx] = useState(null) - const [currUriHasChanges, setCurrUriHasChanges] = useState(false) useEffect(() => { const i = sortedCommandBarURIs.findIndex(e => e.fsPath === uri?.fsPath) - if (i !== -1) { - setUriIdx(i) - setCurrUriHasChanges(true) - } - else { - setCurrUriHasChanges(false) - } + if (i !== -1) { setUriIdx(i) } }, [sortedCommandBarURIs, uri]) const getNextDiffIdx = (step: 1 | -1) => { @@ -131,6 +124,9 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { const isADiffZoneInThisFile = sortedDiffZoneIds.length !== 0 const isADiffZoneInAnyFile = sortedCommandBarURIs.length !== 0 + const streamState = uri ? commandBarService.getStreamState(uri) : null + const showAcceptRejectAll = streamState === 'idle-has-changes' + if (!isADiffZoneInAnyFile) { // no changes for the user to accept return null @@ -303,7 +299,7 @@ const VoidCommandBar = ({ uri, editor }: VoidCommandBarProps) => { // dummy container due to annoyances with VS Code mounting the widget return
- {currUriHasChanges && <> + {showAcceptRejectAll && <>
{acceptAllButton} {rejectAllButton} diff --git a/src/vs/workbench/contrib/void/browser/terminalToolService.ts b/src/vs/workbench/contrib/void/browser/terminalToolService.ts index e8d1edcd..c9cbd178 100644 --- a/src/vs/workbench/contrib/void/browser/terminalToolService.ts +++ b/src/vs/workbench/contrib/void/browser/terminalToolService.ts @@ -9,7 +9,7 @@ import { registerSingleton, InstantiationType } from '../../../../platform/insta import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { TerminalLocation } from '../../../../platform/terminal/common/terminal.js'; import { ITerminalService, ITerminalInstance } from '../../../../workbench/contrib/terminal/browser/terminal.js'; -import { ResolveReason } from '../common/toolsServiceTypes.js'; +import { TerminalResolveReason } from '../common/toolsServiceTypes.js'; import { MAX_TERMINAL_CHARS_PAGE, TERMINAL_BG_WAIT_TIME, TERMINAL_TIMEOUT_TIME } from './toolsService.js'; @@ -18,7 +18,7 @@ export interface ITerminalToolService { readonly _serviceBrand: undefined; listTerminalIds(): string[]; - runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: ResolveReason }>; + runCommand(command: string, proposedTerminalId: string, waitForCompletion: boolean): Promise<{ terminalId: string, didCreateTerminal: boolean, result: string, resolveReason: TerminalResolveReason }>; openTerminal(terminalId: string): Promise terminalExists(terminalId: string): boolean } @@ -162,7 +162,7 @@ export class TerminalToolService extends Disposable implements ITerminalToolServ await this.terminalService.focusActiveInstance() let result: string = '' - let resolveReason: ResolveReason | undefined = undefined + let resolveReason: TerminalResolveReason | undefined = undefined const disposables: IDisposable[] = [] diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index cdc27fe7..271322d5 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<{ result: ToolResultType[T], cancel?: () => void }> } +type CallTool = { [T in ToolName]: (p: ToolCallParams[T]) => Promise<{ result: ToolResultType[T], interruptTool?: () => void }> } type ToolResultToString = { [T in ToolName]: (p: ToolCallParams[T], result: ToolResultType[T]) => string } @@ -357,9 +357,10 @@ export class ToolsService implements IToolsService { if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) const [diffZoneURI, applyDonePromise] = res - const cancel = () => editCodeService.interruptURIStreaming({ uri: diffZoneURI }) - - return { result: applyDonePromise, cancel } + const interruptTool = () => { // must reject the applyPromiseDone promise + editCodeService.interruptURIStreaming({ uri: diffZoneURI }) + } + return { result: applyDonePromise, interruptTool } }, terminal_command: async ({ command, proposedTerminalId, waitForCompletion }) => { const { terminalId, didCreateTerminal, result, resolveReason } = await this.terminalToolService.runCommand(command, proposedTerminalId, waitForCompletion) @@ -393,6 +394,7 @@ export class ToolsService implements IToolsService { return `URI ${params.uri.fsPath} successfully deleted.` }, edit: (params, result) => { + console.log('STR OF RESULT', params) return `Change successfully made to ${params.uri.fsPath}.` }, terminal_command: (params, result) => { diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index d5042dc9..d6c05f13 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -10,17 +10,137 @@ import { CodeSelection, FileSelection, StagingSelectionItem } from '../chatThrea import { ChatMode } from '../voidSettingsTypes.js'; import { IVoidModelService } from '../voidModelService.js'; import { EndOfLinePreference } from '../../../../../editor/common/model.js'; +import { InternalToolInfo } from '../toolsServiceTypes.js'; // this is just for ease of readability export const tripleTick = ['```', '```'] -export const editToolDesc_toolDescription = `\ -Your description should be of the form:\n${tripleTick[0]}\n// ... existing code ...\n{{change 1}}\n// ... existing code ...\n{{change2}}\n// ... existing code ...\n{{change 3}}\n...\n${tripleTick[1]}. \ -Do NOT re-write the whole file, and instead use comments like // ... existing code ... . Write as little as possible. \ -Your description will be handed to a dumber, faster model that will quickly apply the change, so try to be brief, but also make sure to include enough information to accurately describe the change. \ -Include the triple ticks in your output.` +const changesExampleContent = `\ +// ... existing code ... +// {{change 1}} +// // ... existing code ... +// {{change 2}} +// // ... existing code ... +// {{change 3}} +// ... existing code ...` +const editToolDescription = `\ +${tripleTick[0]} +${changesExampleContent} +${tripleTick[1]}` + +const fileNameEdit = `${tripleTick[0]}typescript +/Users/username/Dekstop/my_project/app.ts +${changesExampleContent} +${tripleTick[1]}` + + + + + +// ======================================================== tools ======================================================== + +const paginationHelper = { + desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, + param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, } +} as const + +export const voidTools = { + // --- context-gathering (read/search/list) --- + + read_file: { + name: 'read_file', + description: `Returns file contents of a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + list_dir: { + name: 'list_dir', + description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, + params: { + uri: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + pathname_search: { + name: 'pathname_search', + description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + text_search: { + name: 'text_search', + description: `Returns pathnames of files with an exact match of the query. The query can be any regex. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, + params: { + query: { type: 'string', description: undefined }, + ...paginationHelper.param, + }, + }, + + // --- editing (create/delete) --- + + create_uri: { + name: 'create_uri', + description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, + params: { + uri: { type: 'string', description: undefined }, + }, + }, + + delete_uri: { + name: 'delete_uri', + description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } + }, + }, + + edit: { // APPLY TOOL + name: 'edit', + description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`, + params: { + uri: { type: 'string', description: undefined }, + changeDescription: { + type: 'string', description: `\ +- Your changeDescription should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. +- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible. +- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise. +- You must output your description in triple backticks. +Here's an example of a good description:\n${editToolDescription}.` + } + }, + }, + + terminal_command: { + name: 'terminal_command', + description: `Executes a terminal command.`, + params: { + command: { type: 'string', description: 'The terminal command to execute.' }, + waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` }, + terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' }, + }, + }, + + + // go_to_definition + // go_to_usages + +} satisfies { [name: string]: InternalToolInfo } + + + + + +// ======================================================== chat (normal, gather, agent) ======================================================== @@ -32,17 +152,21 @@ ${mode === 'agent' ? `to help the user develop, run, deploy, and make changes to : ''} 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. The user's query is never invalid. - +${/* system info */''} The user's system information is as follows: - ${os} - Open workspace(s): ${workspaces.join(', ') || 'NO WORKSPACE OPEN'} ${(mode === 'agent') && runningTerminalIds.length !== 0 ? `\ - Existing terminal IDs: ${runningTerminalIds.join(', ')} `: '\n'} -${mode === 'agent' || mode === 'gather' /* tool use */ ? `\ +${/* tool use */ mode === 'agent' || mode === 'gather' ? `\ You will be given tools you can call. +${mode === 'agent' ? `\ - Only use tools if they help you accomplish the user's goal. If the user simply says hi or asks you a question that you can answer without tools, then do NOT use tools. -- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool. +- ALWAYS use tools to take actions. For example, if you would like to edit a file, you MUST use a tool.` + : mode === 'gather' ? `\ +- Your primary use of tools should be to gather information to help the user understand the codebase and answer their query.` + : ''} - If you think you should use tools, you do not need to ask for permission. Feel free to call tools whenever you'd like. You can use them to understand the codebase, ${mode === 'agent' ? 'run terminal commands, edit files, ' : 'gather relevant files and information, '}etc. - NEVER refer to a tool by name when speaking with the user (NEVER say something like "I'm going to use \`tool_name\`"). Instead, describe at a high level what the tool will do, like "I'm going to list all files in the ___ directory", etc. Also do not refer to "pages" of results, just say you're getting more results. - Some tools only work if the user has a workspace open.${mode === 'agent' ? ` @@ -52,20 +176,23 @@ You will be given tools you can call. You're allowed to ask for more context. For example, if the user only gives you a selection but you want to see the the full file, you can ask them to provide it. \ `} -${mode === 'agent' /* code blocks */ ? `\ +${/* code blocks */ mode === 'agent' ? `\ Behavior: -- Prioritize editing files and running commands over simply making suggestions. +- Always use tools (edit, terminal, etc) to take actions and implement changes. Don't just describe them. - Prioritize taking as many steps as you need to complete your request over stopping early.\ `: `\ If you think it's appropriate to suggest an edit to a file, then you must describe your suggestion in CODE BLOCK(S) (wrapped in triple backticks). -- The first line of the code block must be the FULL PATH of the file you want to change. If the path does not already exist, it will be created. -- The remaining contents of the code block will be given to a dumber, faster model that will quickly apply the change. -- Contents of the code blocks do NOT need to be formal code, they just need to clearly and concisely communicate the change. -- Do NOT re-write the entire file in the code block(s). Instead, write comments like "// ... existing code" to indicate how to change the existing code.`} - +- The first line of the code block must be the FULL PATH of the file you want to change. +- The remaining contents should be a brief code description of the change you want to make, with comments like "// ... existing code ..." to condense your writing. +- NEVER re-write the whole file, and ALWAYS use comments like "// ... existing code ...". Bias towards writing as little as possible. +- Your description will be handed to a dumber, faster model that will quickly apply the change, so it should be clear and concise. +Here's an example of a good code block:\n${fileNameEdit}.\ +`} +${/* misc */''} Misc: - Do not make things up. - Do not be lazy. +- NEVER re-write the entire file. - Always wrap any code you produce in triple backticks, and specify a language if possible. For example, ${tripleTick[0]}typescript\n...\n${tripleTick[1]}.\ ` // agent mode doesn't know about 1st line paths yet @@ -181,6 +308,7 @@ Directions: +// ======================================================== apply (writeover) ======================================================== export const rewriteCode_userMessage = ({ originalCode, applyStr, language }: { originalCode: string, applyStr: string, language: string }) => { @@ -202,69 +330,7 @@ Please finish writing the new file by applying the change to the original file. - - - -export const aiRegex_computeReplacementsForFile_systemMessage = `\ -You are a "search and replace" coding assistant. - -You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE. - -The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for. - -The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace. - -The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes. - -## Instructions - -1. If you do not want to make any changes, you should respond with the word "no". - -2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make. -For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name. -- Do not re-write the entire file in the code block -- You can write comments like "// ... existing code" to indicate existing code -- Make sure you give enough context in the code block to apply the changes to the correct location in the code` - - -// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => { - -// // we may want to do this in batches -// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } } - -// const file = await stringifyFileSelections([fileSelection], voidFileService) - -// return `\ -// ## FILE -// ${file} - -// ## SEARCH_CLAUSE -// Here is what the user is searching for: -// ${searchClause} - -// ## REPLACE_CLAUSE -// Here is what the user wants to replace it with: -// ${replaceClause} - -// ## INSTRUCTIONS -// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.` -// } - - - - -// // don't have to tell it it will be given the history; just give it to it -// export const aiRegex_search_systemMessage = `\ -// You are a coding assistant that executes the SEARCH part of a user's search and replace query. - -// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. - -// Output -// - Regex query -// - Files to Include (optional) -// - Files to Exclude? (optional) - -// ` +// ======================================================== apply (fast apply - search/replace) ======================================================== @@ -387,6 +453,8 @@ export const voidPrefixAndSuffix = ({ fullFileStr, startLine, endLine }: { fullF } +// ======================================================== quick edit (ctrl+K) ======================================================== + export type QuickEditFimTagsType = { preTag: string, sufTag: string, @@ -444,10 +512,82 @@ ${tripleTick[1]}).` + + + /* +// ======================================================== ai search/replace ======================================================== -OLD CHAT EXAMPLES: +export const aiRegex_computeReplacementsForFile_systemMessage = `\ +You are a "search and replace" coding assistant. + +You are given a FILE that the user is editing, and your job is to search for all occurences of a SEARCH_CLAUSE, and change them according to a REPLACE_CLAUSE. + +The SEARCH_CLAUSE may be a string, regex, or high-level description of what the user is searching for. + +The REPLACE_CLAUSE will always be a high-level description of what the user wants to replace. + +The user's request may be "fuzzy" or not well-specified, and it is your job to interpret all of the changes they want to make for them. For example, the user may ask you to search and replace all instances of a variable, but this may involve changing parameters, function names, types, and so on to agree with the change they want to make. Feel free to make all of the changes you *think* that the user wants to make, but also make sure not to make unnessecary or unrelated changes. + +## Instructions + +1. If you do not want to make any changes, you should respond with the word "no". + +2. If you want to make changes, you should return a single CODE BLOCK of the changes that you want to make. +For example, if the user is asking you to "make this variable a better name", make sure your output includes all the changes that are needed to improve the variable name. +- Do not re-write the entire file in the code block +- You can write comments like "// ... existing code" to indicate existing code +- Make sure you give enough context in the code block to apply the changes to the correct location in the code` + + + + +// export const aiRegex_computeReplacementsForFile_userMessage = async ({ searchClause, replaceClause, fileURI, voidFileService }: { searchClause: string, replaceClause: string, fileURI: URI, voidFileService: IVoidFileService }) => { + +// // we may want to do this in batches +// const fileSelection: FileSelection = { type: 'File', fileURI, selectionStr: null, range: null, state: { isOpened: false } } + +// const file = await stringifyFileSelections([fileSelection], voidFileService) + +// return `\ +// ## FILE +// ${file} + +// ## SEARCH_CLAUSE +// Here is what the user is searching for: +// ${searchClause} + +// ## REPLACE_CLAUSE +// Here is what the user wants to replace it with: +// ${replaceClause} + +// ## INSTRUCTIONS +// Please return the changes you want to make to the file in a codeblock, or return "no" if you do not want to make changes.` +// } + + + + +// // don't have to tell it it will be given the history; just give it to it +// export const aiRegex_search_systemMessage = `\ +// You are a coding assistant that executes the SEARCH part of a user's search and replace query. + +// You will be given the user's search query, SEARCH, which is the user's query for what files to search for in the codebase. You may also be given the user's REPLACE query for additional context. + +// Output +// - Regex query +// - Files to Include (optional) +// - Files to Exclude? (optional) + +// ` + + + + + + +// ======================================================== old examples ======================================================== Do not tell the user anything about the examples below. Do not assume the user is talking about any of the examples below. diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 0b6f7ef3..adefa286 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -1,5 +1,16 @@ import { URI } from '../../../../base/common/uri.js' -import { editToolDesc_toolDescription } from './prompt/prompts.js'; +import { voidTools } from './prompt/prompts.js'; + + +export type ToolDirectoryItem = { + uri: URI; + name: string; + isDirectory: boolean; + isSymbolicLink: boolean; +} + + +export type TerminalResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number } @@ -15,109 +26,6 @@ export type InternalToolInfo = { - -export type ToolDirectoryItem = { - uri: URI; - name: string; - isDirectory: boolean; - isSymbolicLink: boolean; -} - - -export type ResolveReason = { type: 'toofull' | 'timeout' | 'bgtask' } | { type: 'done', exitCode: number } - - - - - -const paginationHelper = { - desc: `Very large results may be paginated (indicated in the result). Pagination fails gracefully if out of bounds or invalid page number.`, - param: { pageNumber: { type: 'number', description: 'The page number (default is the first page = 1).' }, } -} as const - -export const voidTools = { - // --- context-gathering (read/search/list) --- - - read_file: { - name: 'read_file', - description: `Returns file contents of a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - list_dir: { - name: 'list_dir', - description: `Returns all file names and folder names in a given URI. ${paginationHelper.desc}`, - params: { - uri: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - pathname_search: { - name: 'pathname_search', - description: `Returns all pathnames that match a given grep query. You should use this when looking for a file with a specific name or path. This does NOT search file content. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - text_search: { - name: 'text_search', - description: `Returns pathnames of files with an exact match of the query. The query can be any regex. This does NOT search pathname. As a follow-up, you may want to use read_file to view the full file contents of the results. ${paginationHelper.desc}`, - params: { - query: { type: 'string', description: undefined }, - ...paginationHelper.param, - }, - }, - - // --- editing (create/delete) --- - - create_uri: { - name: 'create_uri', - description: `Create a file or folder at the given path. To create a folder, ensure the path ends with a trailing slash. Fails gracefully if the file already exists. Missing ancestors in the path will be recursively created automatically.`, - params: { - uri: { type: 'string', description: undefined }, - }, - }, - - delete_uri: { - name: 'delete_uri', - description: `Delete a file or folder at the given path. Fails gracefully if the file or folder does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - params: { type: 'string', description: 'Return -r here to delete this URI and all descendants (if applicable). Default is the empty string.' } - }, - }, - - edit: { // APPLY TOOL - name: 'edit', - description: `Edits the contents of a file, given the file's URI and a description. Fails gracefully if the file does not exist.`, - params: { - uri: { type: 'string', description: undefined }, - changeDescription: { type: 'string', description: editToolDesc_toolDescription } // long description here - }, - }, - - terminal_command: { - name: 'terminal_command', - description: `Executes a terminal command.`, - params: { - command: { type: 'string', description: 'The terminal command to execute.' }, - waitForCompletion: { type: 'string', description: `Whether or not to await the command to complete and get the final result. Default is true. Make this value false when you want a command to run indefinitely without waiting for it.` }, - terminalId: { type: 'string', description: 'Optional (value must be an integer >= 1, or empty which will go with the default). This is the ID of the terminal instance to execute the command in. The primary purpose of this is to start a new terminal for background processes or tasks that run indefinitely (e.g. if you want to run a server locally). Fails gracefully if a terminal ID does not exist, by creating a new terminal instance. Defaults to the preferred terminal ID.' }, - }, - }, - - - // go_to_definition - // go_to_usages - -} satisfies { [name: string]: InternalToolInfo } - export type ToolName = keyof typeof voidTools export const toolNames = Object.keys(voidTools) as ToolName[] @@ -151,9 +59,9 @@ export type ToolResultType = { 'pathname_search': { uris: URI[], hasNextPage: boolean }, 'text_search': { uris: URI[], hasNextPage: boolean }, // --- - 'edit': {}, + 'edit': Promise, 'create_uri': {}, 'delete_uri': {}, - 'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: ResolveReason; }, + 'terminal_command': { terminalId: string, didCreateTerminal: boolean, result: string; resolveReason: TerminalResolveReason; }, }