edit_file -> replace_in_file (SEARCH/REPLACE blocks)

This commit is contained in:
Andrew Pareles 2025-04-27 19:57:21 -07:00
parent 19313c7e5d
commit 1e5f9808b4
8 changed files with 367 additions and 346 deletions

View file

@ -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

View file

@ -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<void>] | 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<void>((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<TrackingZone<SearchReplaceDiffAreaMetadata>, '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<TrackingZone<SearchReplaceDiffAreaMetadata>, '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)

View file

@ -44,6 +44,7 @@ export interface IEditCodeService {
callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise<void>;
startApplying(opts: StartApplyingOpts): [URI, Promise<void>] | null;
instantlyApplySearchReplaceBlocks(opts: { uri: URI; searchReplaceBlocks: string }): void;
addCtrlKZone(opts: AddCtrlKOpts): number | undefined;
removeCtrlKZone(opts: { diffareaid: number }): void;

View file

@ -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 <IconShell1
@ -306,18 +300,14 @@ export const ApplyButtonsHTML = ({ codeStr, applyBoxId, reapplyIcon, uri }: { co
if (currStreamState === 'idle-no-changes') {
return <IconShell1
Icon={reapplyIcon ? RotateCw : Play}
Icon={Play}
onClick={onClickSubmit}
{...tooltipPropsForApplyBlock({ tooltipName: reapplyIcon ? 'Reapply' : 'Apply' })}
{...tooltipPropsForApplyBlock({ tooltipName: 'Apply' })}
/>
}
if (currStreamState === 'idle-has-changes') {
return <>
{/* <IconShell1
Icon={RotateCw}
onClick={onReapply}
/> */}
<IconShell1
Icon={X}
onClick={onReject}
@ -375,7 +365,7 @@ export const BlockCodeApplyWrapper = ({
<div className={`${canApply ? '' : 'hidden'} flex items-center gap-1`}>
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={initValue} toolTipName='Copy' />}
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} reapplyIcon={false} />
<ApplyButtonsHTML uri={uri} applyBoxId={applyBoxId} codeStr={initValue} />
</div>
</div>

View file

@ -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 <div className='!select-text cursor-auto'>
<SmallProseWrapper>
<ChatMarkdownRender string={changeDiff} codeURI={uri} chatMessageLocation={undefined} />
<ChatMarkdownRender string={searchReplaceBlocks} codeURI={uri} chatMessageLocation={undefined} />
</SmallProseWrapper>
</div>
}
@ -1513,7 +1513,6 @@ const EditToolHeaderButtons = ({ applyBoxId, uri, codeStr }: { applyBoxId: strin
<StatusIndicatorForApplyButton applyBoxId={applyBoxId} uri={uri} />
<JumpToFileButton uri={uri} />
{currStreamState === 'idle-no-changes' && <CopyButton codeStr={codeStr} toolTipName='Copy' />}
<ApplyButtonsHTML applyBoxId={applyBoxId} uri={uri} codeStr={codeStr} reapplyIcon={true} />
</div>
}
@ -1974,7 +1973,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <ToolHeaderWrapper {...componentParams} />
}
},
'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<T>,
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
changeDiff={params.changeDiff}
searchReplaceBlocks={params.searchReplaceBlocks}
/>
</ToolChildrenWrapper>
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
@ -2009,7 +2008,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={params.changeDiff}
codeStr={params.searchReplaceBlocks}
/>
}
@ -2022,7 +2021,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
changeDiff={params.changeDiff}
searchReplaceBlocks={params.searchReplaceBlocks}
/>
</ToolChildrenWrapper>
}
@ -2039,7 +2038,7 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
{/* content */}
<EditToolChildren
uri={params.uri}
changeDiff={params.changeDiff}
searchReplaceBlocks={params.searchReplaceBlocks}
/>
</ToolChildrenWrapper>
}
@ -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 = <span className='flex items-center'>
@ -2650,7 +2649,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
>
<EditToolChildren
uri={uri}
changeDiff={toolCallSoFar.rawParams.change_diff ?? ''}
searchReplaceBlocks={toolCallSoFar.rawParams.search_replace_blocks ?? ''}
/>
<IconLoading />
</ToolHeaderWrapper>
@ -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' ? <EditToolSoFar
toolCallSoFar.name === 'replace_in_file' ? <EditToolSoFar
key={'curr-streaming-tool'}
toolCallSoFar={toolCallSoFar}
/>

View file

@ -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.`

View file

@ -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]}`

View file

@ -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': {},
// ---