From 1e5f9808b4069676cdd88bd767449bbda3d31d0d Mon Sep 17 00:00:00 2001 From: Andrew Pareles Date: Sun, 27 Apr 2025 19:57:21 -0700 Subject: [PATCH] edit_file -> replace_in_file (SEARCH/REPLACE blocks) --- .../contrib/void/browser/chatThreadService.ts | 2 +- .../contrib/void/browser/editCodeService.ts | 452 ++++++++++-------- .../void/browser/editCodeServiceInterface.ts | 1 + .../src/markdown/ApplyBlockHoverButtons.tsx | 18 +- .../react/src/sidebar-tsx/SidebarChat.tsx | 29 +- .../contrib/void/browser/toolsService.ts | 37 +- .../contrib/void/common/prompt/prompts.ts | 168 ++++--- .../contrib/void/common/toolsServiceTypes.ts | 6 +- 8 files changed, 367 insertions(+), 346 deletions(-) diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index baae23e4..a4444838 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -506,7 +506,7 @@ class ChatThreadService extends Disposable implements IChatThreadService { return {} } // once validated, add checkpoint for edit - if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) } + if (toolName === 'replace_in_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['replace_in_file']).uri }) } // 2. if tool requires approval, break from the loop, awaiting approval diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index b310c355..e21b1776 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -25,7 +25,7 @@ import * as dom from '../../../../base/browser/dom.js'; import { Widget } from '../../../../base/browser/ui/widget.js'; import { URI } from '../../../../base/common/uri.js'; import { IConsistentEditorItemService, IConsistentItemService } from './helperServices/consistentItemService.js'; -import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplace_systemMessage, searchReplace_userMessage, } from '../common/prompt/prompts.js'; +import { voidPrefixAndSuffix, ctrlKStream_userMessage, ctrlKStream_systemMessage, defaultQuickEditFimTags, rewriteCode_systemMessage, rewriteCode_userMessage, searchReplaceGivenDescription_systemMessage, searchReplaceGivenDescription_userMessage, } from '../common/prompt/prompts.js'; import { mountCtrlK } from './react/out/quick-edit-tsx/index.js' import { QuickEditPropsType } from './quickEditActions.js'; @@ -1164,6 +1164,9 @@ class EditCodeService extends Disposable implements IEditCodeService { } + public instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }: { uri: URI, searchReplaceBlocks: string }) { + this._instantlyApplySRBlocks(uri, searchReplaceBlocks) + } private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null { @@ -1509,6 +1512,77 @@ class EditCodeService extends Disposable implements IEditCodeService { } + private _errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { + + 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)}` + : str === `Not unique` ? + `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + : str === 'Has overlap' ? + `The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` + : `` + + // string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has + // const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n') + // const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : '' + // const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : '' + // const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}` + const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.' + const errMsg = `${descStr}\n${soFarStr}` + return errMsg + + } + + + private _instantlyApplySRBlocks(uri: URI, blocksStr: string) { + const blocks = extractSearchReplaceBlocks(blocksStr) + if (blocks.length === 0) throw new Error(`No Search/Replace blocks were received!`) + + const { model } = this._voidModelService.getModel(uri) + if (!model) throw new Error(`Error applying Search/Replace blocks: File does not exist.`) + const modelStr = model.getValue(EndOfLinePreference.LF) + + const replacements: { origStart: number; origEnd: number; block: ExtractedSearchReplaceBlock }[] = [] + for (const b of blocks) { + const i = modelStr.indexOf(b.orig) + if (i === -1) + throw new Error(this._errContentOfInvalidStr('Not found', replacements[i].block.orig)) + const j = modelStr.lastIndexOf(b.orig) + if (i !== j) + throw new Error(this._errContentOfInvalidStr('Not unique', replacements[i].block.orig)) + + replacements.push({ + origStart: i, + origEnd: i + b.orig.length - 1, // INCLUSIVE + block: b, + }) + } + + // sort in increasing order + replacements.sort((a, b) => a.origStart - b.origStart) + + // ensure no overlap + for (let i = 1; i < replacements.length; i++) { + if (replacements[i].origStart < replacements[i - 1].origEnd) { + // There's an overlap + throw new Error(this._errContentOfInvalidStr('Has overlap', replacements[i].block.orig)) + } + } + + // apply each replacement from right to left (so indexes don't shift) + let newCode: string = modelStr + for (let i = replacements.length - 1; i >= 0; i--) { + const { origStart, origEnd, block } = replacements[i] + newCode = newCode.slice(0, origStart) + block.final + newCode.slice(origEnd + 1, Infinity) + } + + this._writeURIText(uri, newCode, + 'wholeFileRange', + { shouldRealignDiffAreas: true } + ) + + } + private _initializeSearchAndReplaceStream(opts: StartApplyingOpts & { from: 'ClickApply' }): [DiffZone, Promise] | undefined { const { from, applyStr, } = opts const featureName: FeatureName = 'Apply' @@ -1526,10 +1600,10 @@ class EditCodeService extends Disposable implements IEditCodeService { // build messages - ask LLM to generate search/replace block text const originalFileCode = model.getValue(EndOfLinePreference.LF) - const userMessageContent = searchReplace_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) + const userMessageContent = searchReplaceGivenDescription_userMessage({ originalCode: originalFileCode, applyStr: applyStr }) const { messages, separateSystemMessage: separateSystemMessage } = this._convertToLLMMessageService.prepareLLMSimpleMessages({ - systemMessage: searchReplace_systemMessage, + systemMessage: searchReplaceGivenDescription_systemMessage, simpleMessages: [{ role: 'user', content: userMessageContent, }], featureName, modelSelection, @@ -1577,27 +1651,6 @@ class EditCodeService extends Disposable implements IEditCodeService { } - const errContentOfInvalidStr = (str: 'Not found' | 'Not unique' | 'Has overlap', blockOrig: string) => { - - 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)}` - : str === `Not unique` ? - `The most recent ORIGINAL code shows up multiple times in the file, so you were interrupted. You might want to expand the ORIGINAL excerpt so it's unique. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` - : str === 'Has overlap' ? - `The most recent ORIGINAL code has overlap with another ORIGINAL code block that you outputted. Do NOT output any overlapping edits. The problematic ORIGINAL code was:\n${JSON.stringify(blockOrig)}` - : `` - - // string of <<<<< ORIGINAL >>>>> REPLACE blocks so far so LLM can understand what it currently has - // const blocksSoFarStr = blocks.slice(0, blockNum).map(block => `${ORIGINAL}\n${block.orig}\n${DIVIDER}\n${block.final}\n${FINAL}`).join('\n') - // const soFarStr = blocksSoFarStr ? `These are the Search/Replace blocks that have been applied so far:${tripleTick[0]}\n${blocksSoFarStr}\n${tripleTick[1]}` : '' - // const continueMsg = soFarStr ? `${soFarStr}Please continue outputting SEARCH/REPLACE blocks starting where this leaves off.` : '' - // const errMsg = `${descStr}${continueMsg ? `\n${continueMsg}` : ''}` - const soFarStr = 'All of your previous outputs have been ignored. Please re-output ALL SEARCH/REPLACE blocks starting from the first one, and avoid the error this time.' - const errMsg = `${descStr}\n${soFarStr}` - return errMsg - - } - const onDone = () => { diffZone._streamState = { isStreaming: false, } this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid }) @@ -1652,6 +1705,159 @@ class EditCodeService extends Disposable implements IEditCodeService { let resMessageDonePromise: () => void = () => { } const messageDonePromise = new Promise((res, rej) => { resMessageDonePromise = res }) + + const onText = (params: { fullText: string; fullReasoning: string }) => { + const { fullText } = params + // blocks are [done done done ... {writingFinal|writingOriginal}] + // ^ + // currStreamingBlockNum + + const blocks = extractSearchReplaceBlocks(fullText) + + for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { + const block = blocks[blockNum] + + if (block.state === 'writingOriginal') { + // update stream state to the first line of original if some portion of original has been written + if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { + const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line + const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine) + if (typeof originalRange !== 'string') { + const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) + diffZone._streamState.line = startLine + shouldUpdateOrigStreamStyle = false + } + } + + // // starting line is at least the number of lines in the generated code minus 1 + // const numLinesInOrig = numLinesOfStr(block.orig) + // const newLine = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1) + // if (newLine !== diffZone._streamState.line) { + // diffZone._streamState.line = newLine + // this._refreshStylesAndDiffsInURI(uri) + // } + + + // must be done writing original to move on to writing streamed content + continue + } + shouldUpdateOrigStreamStyle = true + + + // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it + if (!(blockNum in addedTrackingZoneOfBlockNum)) { + + const originalBounds = findTextInCode(block.orig, originalFileCode, true) + // if error + // Check for overlap with existing modified ranges + const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => { + const [existingStart, existingEnd] = trackingZone.metadata.originalBounds; + const hasNoOverlap = endLine < existingStart || startLine > existingEnd + return !hasNoOverlap + }); + + if (typeof originalBounds === 'string' || hasOverlap) { + const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const + + console.log('--------------Error finding text in code:') + console.log('originalFileCode', { originalFileCode }) + console.log('fullText', { fullText }) + console.log('error:', errorMessage) + console.log('block.orig:', block.orig) + console.log('---------') + const content = this._errContentOfInvalidStr(errorMessage, block.orig) + messages.push( + { role: 'assistant', content: fullText }, // latest output + { role: 'user', content: content } // user explanation of what's wrong + ) + + // REVERT ALL BLOCKS + currStreamingBlockNum = 0 + latestStreamLocationMutable = null + shouldUpdateOrigStreamStyle = true + oldBlocks = [] + for (const trackingZone of addedTrackingZoneOfBlockNum) + this._deleteTrackingZone(trackingZone) + addedTrackingZoneOfBlockNum.splice(0, Infinity) + + this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true }) + + // abort and resolve + shouldSendAnotherMessage = true + if (streamRequestIdRef.current) { + weAreAborting = true + this._llmMessageService.abort(streamRequestIdRef.current) + weAreAborting = false + } + diffZone._streamState.line = 1 + resMessageDonePromise() + this._refreshStylesAndDiffsInURI(uri) + return + } + + + + const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) + + // console.log('---------adding-------') + // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) + // console.log('block', deepClone(block)) + // console.log('origBounds', originalBounds) + // console.log('start end', startLine, endLine) + + // otherwise if no error, add the position as a diffarea + const adding: Omit, 'diffareaid'> = { + type: 'TrackingZone', + startLine: startLine, + endLine: endLine, + _URI: uri, + metadata: { + originalBounds: [...originalBounds], + originalCode: block.orig, + }, + } + const trackingZone = this._addDiffArea(adding) + addedTrackingZoneOfBlockNum.push(trackingZone) + latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } + } // end adding diffarea + + + // should always be in streaming state here + if (!diffZone._streamState.isStreaming) { + console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') + continue + } + + // if a block is done, finish it by writing all + if (block.state === 'done') { + const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum] + this._writeURIText(uri, block.final, + { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed + { shouldRealignDiffAreas: true } + ) + diffZone._streamState.line = finalEndLine + 1 + currStreamingBlockNum = blockNum + 1 + continue + } + + // write the added text to the file + if (!latestStreamLocationMutable) continue + const oldBlock = oldBlocks[blockNum] + const oldFinalLen = (oldBlock?.final ?? '').length + const deltaFinalText = block.final.substring(oldFinalLen, Infinity) + + this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) + oldBlocks = blocks // oldblocks is only used if writingFinal + + // const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable + // diffZone._streamState.line = currentEndLine + diffZone._streamState.line = latestStreamLocationMutable.line + + } // end for + + this._refreshStylesAndDiffsInURI(uri) + } + streamRequestIdRef.current = this._llmMessageService.sendLLMMessage({ messagesType: 'chatMessages', logging: { loggingName: `Edit (Search/Replace) - ${from}` }, @@ -1661,201 +1867,25 @@ class EditCodeService extends Disposable implements IEditCodeService { separateSystemMessage, chatMode: null, // not chat onText: (params) => { - const { fullText } = params - // blocks are [done done done ... {writingFinal|writingOriginal}] - // ^ - // currStreamingBlockNum - - const blocks = extractSearchReplaceBlocks(fullText) - - for (let blockNum = currStreamingBlockNum; blockNum < blocks.length; blockNum += 1) { - const block = blocks[blockNum] - - if (block.state === 'writingOriginal') { - // update stream state to the first line of original if some portion of original has been written - if (shouldUpdateOrigStreamStyle && block.orig.trim().length >= 20) { - const startingAtLine = diffZone._streamState.line ?? 1 // dont go backwards if already have a stream line - const originalRange = findTextInCode(block.orig, originalFileCode, false, startingAtLine) - if (typeof originalRange !== 'string') { - const [startLine, _] = convertOriginalRangeToFinalRange(originalRange) - diffZone._streamState.line = startLine - shouldUpdateOrigStreamStyle = false - } - } - - // // starting line is at least the number of lines in the generated code minus 1 - // const numLinesInOrig = numLinesOfStr(block.orig) - // const newLine = Math.max(numLinesInOrig - 1, 1, diffZone._streamState.line ?? 1) - // if (newLine !== diffZone._streamState.line) { - // diffZone._streamState.line = newLine - // this._refreshStylesAndDiffsInURI(uri) - // } - - - // must be done writing original to move on to writing streamed content - continue - } - shouldUpdateOrigStreamStyle = true - - - // if this is the first time we're seeing this block, add it as a diffarea so we can start streaming in it - if (!(blockNum in addedTrackingZoneOfBlockNum)) { - - const originalBounds = findTextInCode(block.orig, originalFileCode, true) - // if error - // Check for overlap with existing modified ranges - const hasOverlap = addedTrackingZoneOfBlockNum.some(trackingZone => { - const [existingStart, existingEnd] = trackingZone.metadata.originalBounds; - const hasNoOverlap = endLine < existingStart || startLine > existingEnd - return !hasNoOverlap - }); - - if (typeof originalBounds === 'string' || hasOverlap) { - const errorMessage = typeof originalBounds === 'string' ? originalBounds : 'Has overlap' as const - - console.log('--------------Error finding text in code:') - console.log('originalFileCode', { originalFileCode }) - console.log('fullText', { fullText }) - console.log('error:', errorMessage) - console.log('block.orig:', block.orig) - console.log('---------') - const content = errContentOfInvalidStr(errorMessage, block.orig) - messages.push( - { role: 'assistant', content: fullText }, // latest output - { role: 'user', content: content } // user explanation of what's wrong - ) - - // REVERT ALL BLOCKS - currStreamingBlockNum = 0 - latestStreamLocationMutable = null - shouldUpdateOrigStreamStyle = true - oldBlocks = [] - for (const trackingZone of addedTrackingZoneOfBlockNum) - this._deleteTrackingZone(trackingZone) - addedTrackingZoneOfBlockNum.splice(0, Infinity) - - this._writeURIText(uri, originalFileCode, 'wholeFileRange', { shouldRealignDiffAreas: true }) - - // abort and resolve - shouldSendAnotherMessage = true - if (streamRequestIdRef.current) { - weAreAborting = true - this._llmMessageService.abort(streamRequestIdRef.current) - weAreAborting = false - } - diffZone._streamState.line = 1 - resMessageDonePromise() - this._refreshStylesAndDiffsInURI(uri) - return - } - - - - const [startLine, endLine] = convertOriginalRangeToFinalRange(originalBounds) - - // console.log('---------adding-------') - // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) - // console.log('block', deepClone(block)) - // console.log('origBounds', originalBounds) - // console.log('start end', startLine, endLine) - - // otherwise if no error, add the position as a diffarea - const adding: Omit, 'diffareaid'> = { - type: 'TrackingZone', - startLine: startLine, - endLine: endLine, - _URI: uri, - metadata: { - originalBounds: [...originalBounds], - originalCode: block.orig, - }, - } - const trackingZone = this._addDiffArea(adding) - addedTrackingZoneOfBlockNum.push(trackingZone) - latestStreamLocationMutable = { line: startLine, addedSplitYet: false, col: 1, originalCodeStartLine: 1 } - } // end adding diffarea - - - // should always be in streaming state here - if (!diffZone._streamState.isStreaming) { - console.error('DiffZone was not in streaming state in _initializeSearchAndReplaceStream') - continue - } - - // if a block is done, finish it by writing all - if (block.state === 'done') { - const { startLine: finalStartLine, endLine: finalEndLine } = addedTrackingZoneOfBlockNum[blockNum] - this._writeURIText(uri, block.final, - { startLineNumber: finalStartLine, startColumn: 1, endLineNumber: finalEndLine, endColumn: Number.MAX_SAFE_INTEGER }, // 1-indexed - { shouldRealignDiffAreas: true } - ) - diffZone._streamState.line = finalEndLine + 1 - currStreamingBlockNum = blockNum + 1 - continue - } - - // write the added text to the file - if (!latestStreamLocationMutable) continue - const oldBlock = oldBlocks[blockNum] - const oldFinalLen = (oldBlock?.final ?? '').length - const deltaFinalText = block.final.substring(oldFinalLen, Infinity) - - this._writeStreamedDiffZoneLLMText(uri, block.orig, block.final, deltaFinalText, latestStreamLocationMutable) - oldBlocks = blocks // oldblocks is only used if writingFinal - - // const { endLine: currentEndLine } = addedTrackingZoneOfBlockNum[blockNum] // would be bad to do this because a lot of the bottom lines might be the same. more accurate to go with latestStreamLocationMutable - // diffZone._streamState.line = currentEndLine - diffZone._streamState.line = latestStreamLocationMutable.line - - } // end for - - this._refreshStylesAndDiffsInURI(uri) + onText(params) }, onFinalMessage: async (params) => { const { fullText } = params + onText(params) - - // 1. wait 500ms and fix lint errors - call lint error workflow - // (update react state to say "Fixing errors") const blocks = extractSearchReplaceBlocks(fullText) - if (blocks.length === 0) { - this._notificationService.info(`Void: We ran Apply, but the LLM didn't output any changes.`) - } - // writeover the whole file - let newCode = originalFileCode - - // IMPORTANT - sort by lineNum - addedTrackingZoneOfBlockNum.sort((a, b) => a.metadata.originalBounds[0] - b.metadata.originalBounds[0]) - - // const { model } = this._voidModelService.getModel(uri) - // console.log('DONE - editCode!', { fullText }) - // console.log('CURRENT TEXT!!!', { current: model?.getValue() }) - // console.log('addedTrackingZoneOfBlockNum', addedTrackingZoneOfBlockNum) - // console.log('blocks', deepClone(blocks)) - - for (let blockNum = addedTrackingZoneOfBlockNum.length - 1; blockNum >= 0; blockNum -= 1) { - const { originalBounds } = addedTrackingZoneOfBlockNum[blockNum].metadata - const finalCode = blocks[blockNum].final - - if (finalCode === null) continue - - const [originalStart, originalEnd] = originalBounds - const lines = newCode.split('\n') - newCode = [ - ...lines.slice(0, (originalStart - 1)), - ...finalCode.split('\n'), - ...lines.slice((originalEnd - 1) + 1, Infinity) - ].join('\n') + this._notificationService.info(`Void: We ran Fast Apply, but the LLM didn't output any changes.`) } - this._writeURIText(uri, newCode, - 'wholeFileRange', - { shouldRealignDiffAreas: true } - ) - - onDone() - resMessageDonePromise() + try { + this._instantlyApplySRBlocks(uri, fullText) + onDone() + resMessageDonePromise() + } + catch (e) { + onError(e) + } }, onError: (e) => { onError(e) diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index a63fde7e..5ad58a33 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -44,6 +44,7 @@ export interface IEditCodeService { callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; startApplying(opts: StartApplyingOpts): [URI, Promise] | null; + instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void; addCtrlKZone(opts: AddCtrlKOpts): number | undefined; removeCtrlKZone(opts: { diffareaid: number }): void; 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 a6858032..ab664332 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 @@ -231,7 +231,7 @@ export const StatusIndicatorForApplyButton = ({ applyBoxId, uri }: { applyBoxId: } -export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { codeStr: string, applyBoxId: string, reapplyIcon: boolean, uri: URI | 'current' }) => { +export const ApplyButtonsHTML = ({ codeStr, applyBoxId, uri }: { codeStr: string, applyBoxId: string, uri: URI | 'current' }) => { const accessor = useAccessor() const editCodeService = accessor.get('IEditCodeService') const metricsService = accessor.get('IMetricsService') @@ -287,12 +287,6 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co if (uri) editCodeService.acceptOrRejectAllDiffAreas({ uri, behavior: 'reject', removeCtrlKs: false }) }, [applyBoxId, editCodeService]) - // const onReapply = useCallback(() => { - // onReject() - // onClickSubmit() - // }, [onReject, onClickSubmit]) - - if (currStreamState === 'streaming') { return } if (currStreamState === 'idle-has-changes') { return <> - {/* */} {currStreamState === 'idle-no-changes' && } - + 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 609215b3..78d4dcdf 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 @@ -1239,7 +1239,7 @@ const titleOfToolName = { 'search_for_files': { done: 'Searched', proposed: 'Search', running: loadingTitleWrapper('Searching') }, 'create_file_or_folder': { done: `Created`, proposed: `Create`, running: loadingTitleWrapper(`Creating`) }, 'delete_file_or_folder': { done: `Deleted`, proposed: `Delete`, running: loadingTitleWrapper(`Deleting`) }, - 'edit_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, + 'replace_in_file': { done: `Edited file`, proposed: 'Edit file', running: loadingTitleWrapper('Editing file') }, 'run_command': { done: `Ran terminal`, proposed: 'Run terminal', running: loadingTitleWrapper('Running terminal') }, 'open_persistent_terminal': { done: `Opened terminal`, proposed: 'Open terminal', running: loadingTitleWrapper('Opening terminal') }, 'kill_persistent_terminal': { done: `Killed terminal`, proposed: 'Kill terminal', running: loadingTitleWrapper('Killing terminal') }, @@ -1315,8 +1315,8 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName desc1Info: getRelative(toolParams.uri, accessor), } }, - 'edit_file': () => { - const toolParams = _toolParams as ToolCallParams['edit_file'] + 'replace_in_file': () => { + const toolParams = _toolParams as ToolCallParams['replace_in_file'] return { desc1: getBasename(toolParams.uri.fsPath), desc1Info: getRelative(toolParams.uri, accessor), @@ -1459,10 +1459,10 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }: -const EditToolChildren = ({ uri, changeDiff }: { uri: URI | undefined, changeDiff: string }) => { +const EditToolChildren = ({ uri, searchReplaceBlocks }: { uri: URI | undefined, searchReplaceBlocks: string }) => { return
- +
} @@ -1513,7 +1513,6 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin {currStreamState === 'idle-no-changes' && } - } @@ -1974,7 +1973,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, - 'edit_file': { + 'replace_in_file': { resultWrapper: ({ toolMessage, messageIdx, threadId }) => { const accessor = useAccessor() const isError = toolMessage.type === 'tool_error' @@ -1992,7 +1991,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.children = componentParams.desc2 = @@ -2009,7 +2008,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.desc2 = } @@ -2022,7 +2021,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, componentParams.children = } @@ -2039,7 +2038,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, {/* content */} } @@ -2632,7 +2631,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => const uri = URI.file(toolCallSoFar.rawParams.uri ?? 'unknown') - const title = titleOfToolName['edit_file'].proposed + const title = titleOfToolName['replace_in_file'].proposed const uriDone = toolCallSoFar.doneParams.includes('uri') const desc1 = @@ -2650,7 +2649,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => > @@ -2700,7 +2699,7 @@ export const SidebarChat = () => { const reasoningSoFar = currThreadStreamState?.reasoningSoFar // this is just if it's currently being generated, NOT if it's currently running - const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'edit_file' // show loading for slow tools (right now just edit) + const toolIsGenerating = toolCallSoFar && !toolCallSoFar.isDone && toolCallSoFar.name === 'replace_in_file' // show loading for slow tools (right now just edit) // ----- SIDEBAR CHAT state (local) ----- @@ -2791,7 +2790,7 @@ export const SidebarChat = () => { // the tool currently being generated const generatingTool = toolIsGenerating ? - toolCallSoFar.name === 'edit_file' ? diff --git a/src/vs/workbench/contrib/void/browser/toolsService.ts b/src/vs/workbench/contrib/void/browser/toolsService.ts index be394b7b..e3773f1e 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -11,7 +11,6 @@ import { ITerminalToolService } from './terminalToolService.js' import { LintErrorItem, ToolCallParams, ToolResultType } from '../common/toolsServiceTypes.js' import { IVoidModelService } from '../common/voidModelService.js' import { EndOfLinePreference } from '../../../../editor/common/model.js' -import { basename } from '../../../../base/common/path.js' import { IVoidCommandBarService } from './voidCommandBarService.js' import { computeDirectoryTree1Deep, IDirectoryStrService, stringifyDirectoryTree1Deep } from './directoryStrService.js' import { IMarkerService, MarkerSeverity } from '../../../../platform/markers/common/markers.js' @@ -37,6 +36,7 @@ const isFalsy = (u: unknown) => { } const validateStr = (argName: string, value: unknown) => { + if (value === null) return `Invalid LLM output: ${argName} was null.` if (typeof value !== 'string') throw new Error(`Invalid LLM output format: ${argName} must be a string, but it's a ${typeof value}. Value: ${value}.`) return value } @@ -241,11 +241,12 @@ export class ToolsService implements IToolsService { return { uri, isRecursive, isFolder } }, - edit_file: (params: RawToolParamsObj) => { - const { uri: uriStr, change_diff: changeDiffUnknown } = params + replace_in_file: (params: RawToolParamsObj) => { + const { uri: uriStr, search_replace_blocks: searchReplaceBlocksUnknown } = params const uri = validateURI(uriStr) - const changeDiff = validateStr('changeDiff', changeDiffUnknown) - return { uri, changeDiff } + const searchReplaceBlocks = validateStr('searchReplaceBlocks', searchReplaceBlocksUnknown) + console.log('params!!!', uri, searchReplaceBlocks, 'nnnnn', searchReplaceBlocksUnknown) + return { uri, searchReplaceBlocks } }, // --- @@ -383,36 +384,22 @@ export class ToolsService implements IToolsService { await fileService.del(uri, { recursive: isRecursive }) return { result: {} } }, - - edit_file: async ({ uri, changeDiff }) => { + replace_in_file: async ({ uri, searchReplaceBlocks }) => { await voidModelService.initializeModel(uri) if (this.commandBarService.getStreamState(uri) === 'streaming') { throw new Error(`Another LLM is currently making changes to this file. Please stop streaming for now and ask the user to resume later.`) } - const opts = { - uri, - applyStr: changeDiff, - from: 'ClickApply', - startBehavior: 'keep-conflicts', - } as const - - await editCodeService.callBeforeStartApplying(opts) - const res = editCodeService.startApplying(opts) - if (!res) throw new Error(`The Apply model did not start running on ${basename(uri.fsPath)}. Please try again.`) - const [diffZoneURI, applyDonePromise] = res - - const interruptTool = () => { // must reject the applyPromiseDone promise - editCodeService.interruptURIStreaming({ uri: diffZoneURI }) - } + console.log('aaaa', searchReplaceBlocks) + editCodeService.instantlyApplySearchReplaceBlocks({ uri, searchReplaceBlocks }) // at end, get lint errors - const lintErrorsPromise = applyDonePromise.then(async () => { + const lintErrorsPromise = Promise.resolve().then(async () => { await timeout(2000) const { lintErrors } = this._getLintErrors(uri) return { lintErrors } }) - return { result: lintErrorsPromise, interruptTool } + return { result: lintErrorsPromise } }, // --- run_command: async ({ command, bgTerminalId }) => { @@ -484,7 +471,7 @@ export class ToolsService implements IToolsService { delete_file_or_folder: (params, result) => { return `URI ${params.uri.fsPath} successfully deleted.` }, - edit_file: (params, result) => { + replace_in_file: (params, result) => { const lintErrsString = ( this.voidSettingsService.state.globalSettings.includeToolLintErrors ? (result.lintErrors ? ` Lint errors found after change:\n${stringifyLintErrors(result.lintErrors)}.\nIf this is related to a change made while calling this tool, you might want to fix the error.` diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index 7051ac76..3a4b8ea0 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -33,6 +33,85 @@ export const MAX_TERMINAL_INACTIVE_TIME = 8 // seconds export const MAX_PREFIX_SUFFIX_CHARS = 20_000 + +export const ORIGINAL = `<<<<<<< ORIGINAL` +export const DIVIDER = `=======` +export const FINAL = `>>>>>>> UPDATED` + + + +const searchReplaceBlockTemplate = `\ +${tripleTick[0]} +${ORIGINAL} +// ... original code goes here +${DIVIDER} +// ... final code goes here +${FINAL} +${tripleTick[1]}` + + + + +const createSearchReplaceBlocks_systemMessage = `\ +You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff. +The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`. + +Format your SEARCH/REPLACE blocks as follows: +${searchReplaceBlockTemplate} + +1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out. + +2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change. + +3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output. + +4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. + +5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. + +6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. + +7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. + +## EXAMPLE 1 +DIFF +${tripleTick[0]} +// ... existing code +let x = 6.5 +// ... existing code +${tripleTick[1]} + +ORIGINAL_FILE +${tripleTick[0]} +let w = 5 +let x = 6 +let y = 7 +let z = 8 +${tripleTick[1]} + +ACCEPTED OUTPUT +${tripleTick[0]} +${ORIGINAL} +let x = 6 +${DIVIDER} +let x = 6.5 +${FINAL} +${tripleTick[1]}` + + +const replaceTool_description = `\ +Output a single string of SEARCH/REPLACE block(s) here. Your string should be wrapped in triple backticks. Here's how to format your SEARCH/REPLACE blocks: +${searchReplaceBlockTemplate} + +1. You are allowed to output multiple SEARCH/REPLACE blocks to implement your desired change. Just write them sequentially. + +2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. + +3. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. + +4. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.` + + // ======================================================== tools ======================================================== const changesExampleContent = `\ // ... existing code ... @@ -43,10 +122,10 @@ const changesExampleContent = `\ // {{change 3}} // ... existing code ...` -const editToolDescriptionExample = `\ -${tripleTick[0]} -${changesExampleContent} -${tripleTick[1]}` +// const editToolDescriptionExample = `\ +// ${tripleTick[0]} +// ${changesExampleContent} +// ${tripleTick[1]}` const fileNameEditExample = `${tripleTick[0]}typescript /Users/username/Dekstop/my_project/app.ts @@ -199,26 +278,18 @@ export const voidTools = { }, }, - edit_file: { // APPLY TOOL - name: 'edit_file', - description: `Edits the contents of a file given the file's URI and a description.`, + replace_in_file: { // APPLY TOOL + name: 'replace_in_file', + description: `Edit the contents of a file. You must provide the file's URI as well as SEARCH/REPLACE block(s) that will be used to apply the edit.`, params: { ...uriParam('file'), - change_diff: { - description: `\ -A code diff describing the change to make to the file. \ -Your DIFF is the only context that will be given to another LLM to apply the change, so it must be accurate and complete. \ -Your DIFF MUST be wrapped in triple backticks. \ -NEVER re-write the whole file. Always bias towards writing as little as possible. \ -Use comments like "// ... existing code ..." to condense your writing. \ -Here's an example of a good output:\n${editToolDescriptionExample}` - } + search_replace_blocks: { description: replaceTool_description } }, }, run_command: { name: 'run_command', - description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use edit_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, + description: `Runs a terminal command and waits for the result (times out after ${MAX_TERMINAL_INACTIVE_TIME}s of inactivity). You can use this tool to run any command: sed, grep, etc. Do not edit any files with this tool; use replace_in_file instead. When working with git and other tools that open an editor (e.g. git diff), you should pipe to cat to get all results and not get stuck in vim.`, params: { command: { description: 'The terminal command to run.' }, bg_terminal_id: { description: 'Optional. This only applies to terminals that have been opened with open_persistent_terminal. Runs the command in the terminal with the specified ID.' }, @@ -502,74 +573,17 @@ Please finish writing the new file by applying the change to the original file. // ======================================================== apply (fast apply - search/replace) ======================================================== +export const searchReplaceGivenDescription_systemMessage = createSearchReplaceBlocks_systemMessage -export const ORIGINAL = `<<<<<<< ORIGINAL` -export const DIVIDER = `=======` -export const FINAL = `>>>>>>> UPDATED` - -export const searchReplace_systemMessage = `\ -You are a coding assistant that takes in a diff, and outputs SEARCH/REPLACE code blocks to implement the change(s) in the diff. -The diff will be labeled \`DIFF\` and the original file will be labeled \`ORIGINAL_FILE\`. - -Format your SEARCH/REPLACE blocks as follows: -${tripleTick[0]} -${ORIGINAL} -// ... original code goes here -${DIVIDER} -// ... final code goes here -${FINAL} -${tripleTick[1]} - -1. Your SEARCH/REPLACE block(s) must implement the diff EXACTLY. Do NOT leave anything out. - -2. You are allowed to output multiple SEARCH/REPLACE blocks to implement the change. - -3. Assume any comments in the diff are PART OF THE CHANGE. Include them in the output. - -4. Your output should consist ONLY of SEARCH/REPLACE blocks. Do NOT output any text or explanations before or after this. - -5. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace, comments, or modifications from the original code. - -6. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However; bias towards writing as little as possible. - -7. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text. - -## EXAMPLE 1 -DIFF -${tripleTick[0]} -// ... existing code -let x = 6.5 -// ... existing code -${tripleTick[1]} - -ORIGINAL_FILE -${tripleTick[0]} -let w = 5 -let x = 6 -let y = 7 -let z = 8 -${tripleTick[1]} - -## ACCEPTED OUTPUT -${tripleTick[0]} -${ORIGINAL} -let x = 6 -${DIVIDER} -let x = 6.5 -${FINAL} -${tripleTick[1]} -` - -export const searchReplace_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ +export const searchReplaceGivenDescription_userMessage = ({ originalCode, applyStr }: { originalCode: string, applyStr: string }) => `\ DIFF ${applyStr} ORIGINAL_FILE ${tripleTick[0]} ${originalCode} -${tripleTick[1]} -` +${tripleTick[1]}` diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index b234689c..8972c4ff 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -19,7 +19,7 @@ export type ShallowDirectoryItem = { export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = { 'create_file_or_folder': 'edits', 'delete_file_or_folder': 'edits', - 'edit_file': 'edits', + 'replace_in_file': 'edits', 'run_command': 'terminal', } @@ -42,7 +42,7 @@ export type ToolCallParams = { 'search_in_file': { uri: URI, query: string, isRegex: boolean }, 'read_lint_errors': { uri: URI }, // --- - 'edit_file': { uri: URI, changeDiff: string }, + 'replace_in_file': { uri: URI, searchReplaceBlocks: string }, 'create_file_or_folder': { uri: URI, isFolder: boolean }, 'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean }, // --- @@ -61,7 +61,7 @@ export type ToolResultType = { 'search_in_file': { lines: number[]; }, 'read_lint_errors': { lintErrors: LintErrorItem[] | null }, // --- - 'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>, + 'replace_in_file': Promise<{ lintErrors: LintErrorItem[] | null }>, 'create_file_or_folder': {}, 'delete_file_or_folder': {}, // ---