rewrite file tool

This commit is contained in:
Andrew Pareles 2025-04-27 21:36:47 -07:00
parent 0bb948ee2d
commit 039d5cb812
7 changed files with 183 additions and 93 deletions

View file

@ -507,6 +507,7 @@ class ChatThreadService extends Disposable implements IChatThreadService {
}
// once validated, add checkpoint for edit
if (toolName === 'edit_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['edit_file']).uri }) }
if (toolName === 'rewrite_file') { this._addToolEditCheckpoint({ threadId, uri: (toolParams as ToolCallParams['rewrite_file']).uri }) }
// 2. if tool requires approval, break from the loop, awaiting approval

View file

@ -1192,6 +1192,31 @@ class EditCodeService extends Disposable implements IEditCodeService {
}
public instantlyApplyNewContent({ uri, newContent }: { uri: URI, newContent: string }) {
// start diffzone
const res = this._startStreamingDiffZone({
uri,
streamRequestIdRef: { current: null },
startBehavior: 'keep-conflicts',
linkedCtrlKZone: null,
onWillUndo: () => { },
})
if (!res) return
const { diffZone, onFinishEdit } = res
const onDone = () => {
diffZone._streamState = { isStreaming: false, }
this._onDidChangeStreamingInDiffZone.fire({ uri, diffareaid: diffZone.diffareaid })
this._refreshStylesAndDiffsInURI(uri)
onFinishEdit()
}
this._writeURIText(uri, newContent, 'wholeFileRange', { shouldRealignDiffAreas: false })
onDone()
}
private _findOverlappingDiffArea({ startLine, endLine, uri, filter }: { startLine: number, endLine: number, uri: URI, filter?: (diffArea: DiffArea) => boolean }): DiffArea | null {
// check if there's overlap with any other diffAreas and return early if there is
for (const diffareaid of this.diffAreasOfURI[uri.fsPath] || []) {

View file

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

View file

@ -808,6 +808,84 @@ const ToolHeaderWrapper = ({
const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters<ResultWrapper<'edit_file' | 'rewrite_file'>>[0] & { content: string }) => {
const accessor = useAccessor()
const isError = toolMessage.type === 'tool_error'
const isRejected = toolMessage.type === 'rejected'
const title = getTitle(toolMessage)
const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor)
const icon = null
const { rawParams, params } = toolMessage
const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, }
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
}
else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
// add apply box
if (params) {
const applyBoxId = getApplyBoxId({
threadId: threadId,
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={content}
/>
}
// add children
if (toolMessage.type !== 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
}
else {
// error
const { result } = toolMessage
if (params) {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
{/* error */}
<CodeChildren>
{result}
</CodeChildren>
{/* content */}
<EditToolChildren
uri={params.uri}
code={content}
/>
</ToolChildrenWrapper>
}
else {
componentParams.children = <CodeChildren>
{result}
</CodeChildren>
}
}
}
return <ToolHeaderWrapper {...componentParams} />
}
const SimplifiedToolHeader = ({
title,
@ -1243,6 +1321,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`) },
'rewrite_file': { done: `Rewrote file`, proposed: 'Rewrite file', running: loadingTitleWrapper('Rewriting file') },
'edit_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') },
@ -1319,6 +1398,13 @@ const toolNameToDesc = (toolName: ToolName, _toolParams: ToolCallParams[ToolName
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'rewrite_file': () => {
const toolParams = _toolParams as ToolCallParams['rewrite_file']
return {
desc1: getBasename(toolParams.uri.fsPath),
desc1Info: getRelative(toolParams.uri, accessor),
}
},
'edit_file': () => {
const toolParams = _toolParams as ToolCallParams['edit_file']
return {
@ -1463,10 +1549,10 @@ export const ListableToolItem = ({ name, onClick, isSmall, className, showDot }:
const EditToolChildren = ({ uri, searchReplaceBlocks }: { uri: URI | undefined, searchReplaceBlocks: string }) => {
const EditToolChildren = ({ uri, code }: { uri: URI | undefined, code: string }) => {
return <div className='!select-text cursor-auto'>
<SmallProseWrapper>
<ChatMarkdownRender string={searchReplaceBlocks} codeURI={uri} chatMessageLocation={undefined} />
<ChatMarkdownRender string={code} codeURI={uri} chatMessageLocation={undefined} />
</SmallProseWrapper>
</div>
}
@ -1977,84 +2063,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper<T>,
return <ToolHeaderWrapper {...componentParams} />
}
},
'rewrite_file': {
resultWrapper: (params) => {
return <EditTool {...params} content={params.toolMessage.params.newContent} />
}
},
'edit_file': {
resultWrapper: ({ toolMessage, messageIdx, threadId }) => {
const accessor = useAccessor()
const isError = toolMessage.type === 'tool_error'
const isRejected = toolMessage.type === 'rejected'
const title = getTitle(toolMessage)
const { desc1, desc1Info } = toolNameToDesc(toolMessage.name, toolMessage.params, accessor)
const icon = null
const { rawParams, params } = toolMessage
const componentParams: ToolHeaderParams = { title, desc1, desc1Info, isError, icon, isRejected, }
if (toolMessage.type === 'running_now' || toolMessage.type === 'tool_request') {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
searchReplaceBlocks={params.searchReplaceBlocks}
/>
</ToolChildrenWrapper>
componentParams.desc2 = <JumpToFileButton uri={params.uri} />
}
else if (toolMessage.type === 'success' || toolMessage.type === 'rejected' || toolMessage.type === 'tool_error') {
// add apply box
if (params) {
const applyBoxId = getApplyBoxId({
threadId: threadId,
messageIdx: messageIdx,
tokenIdx: 'N/A',
})
componentParams.desc2 = <EditToolHeaderButtons
applyBoxId={applyBoxId}
uri={params.uri}
codeStr={params.searchReplaceBlocks}
/>
}
// add children
if (toolMessage.type !== 'tool_error') {
const { result } = toolMessage
componentParams.bottomChildren = <EditToolLintErrors lintErrors={result?.lintErrors || []} />
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
<EditToolChildren
uri={params.uri}
searchReplaceBlocks={params.searchReplaceBlocks}
/>
</ToolChildrenWrapper>
}
else {
// error
const { result } = toolMessage
if (params) {
componentParams.children = <ToolChildrenWrapper className='bg-void-bg-3'>
{/* error */}
<CodeChildren>
{result}
</CodeChildren>
{/* content */}
<EditToolChildren
uri={params.uri}
searchReplaceBlocks={params.searchReplaceBlocks}
/>
</ToolChildrenWrapper>
}
else {
componentParams.children = <CodeChildren>
{result}
</CodeChildren>
}
}
}
return <ToolHeaderWrapper {...componentParams} />
resultWrapper: (params) => {
return <EditTool {...params} content={params.toolMessage.params.searchReplaceBlocks} />
}
},
@ -2635,7 +2651,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
const uri = toolCallSoFar.rawParams.uri ? URI.file(toolCallSoFar.rawParams.uri) : undefined
const title = titleOfToolName['edit_file'].proposed
const title = titleOfToolName[toolCallSoFar.name].proposed
const uriDone = toolCallSoFar.doneParams.includes('uri')
const desc1 = <span className='flex items-center'>
@ -2653,7 +2669,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) =>
>
<EditToolChildren
uri={uri}
searchReplaceBlocks={toolCallSoFar.rawParams.search_replace_blocks ?? ''}
code={toolCallSoFar.rawParams.search_replace_blocks ?? toolCallSoFar.rawParams.new_content ?? ''}
/>
<IconLoading />
</ToolHeaderWrapper>
@ -2794,7 +2810,7 @@ export const SidebarChat = () => {
// the tool currently being generated
const generatingTool = toolIsGenerating ?
toolCallSoFar.name === 'edit_file' ? <EditToolSoFar
toolCallSoFar.name === 'edit_file' || toolCallSoFar.name === 'rewrite_file' ? <EditToolSoFar
key={'curr-streaming-tool'}
toolCallSoFar={toolCallSoFar}
/>

View file

@ -36,8 +36,8 @@ 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}.`)
if (value === null) throw new Error(`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(n) ${typeof value}. Value: ${JSON.stringify(value)}.`)
return value
}
@ -45,7 +45,8 @@ const validateStr = (argName: string, value: unknown) => {
// We are NOT checking to make sure in workspace
// TODO!!!! check to make sure folder/file exists
const validateURI = (uriStr: unknown) => {
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a ${typeof uriStr}. Value: ${uriStr}.`)
if (uriStr === null) throw new Error(`Invalid LLM output: uri was null.`)
if (typeof uriStr !== 'string') throw new Error(`Invalid LLM output format: Provided uri must be a string, but it's a(n) ${typeof uriStr}. Value: ${uriStr}.`)
const uri = URI.file(uriStr)
return uri
}
@ -241,6 +242,13 @@ export class ToolsService implements IToolsService {
return { uri, isRecursive, isFolder }
},
rewrite_file: (params: RawToolParamsObj) => {
const { uri: uriStr, new_content: newContentUnknown } = params
const uri = validateURI(uriStr)
const newContent = validateStr('newContent', newContentUnknown)
return { uri, newContent }
},
edit_file: (params: RawToolParamsObj) => {
const { uri: uriStr, search_replace_blocks: searchReplaceBlocksUnknown } = params
const uri = validateURI(uriStr)
@ -383,6 +391,22 @@ export class ToolsService implements IToolsService {
await fileService.del(uri, { recursive: isRecursive })
return { result: {} }
},
rewrite_file: async ({ uri, newContent }) => {
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.`)
}
editCodeService.instantlyApplyNewContent({ uri, newContent })
// at end, get lint errors
const lintErrorsPromise = Promise.resolve().then(async () => {
await timeout(2000)
const { lintErrors } = this._getLintErrors(uri)
return { lintErrors }
})
return { result: lintErrorsPromise }
},
edit_file: async ({ uri, searchReplaceBlocks }) => {
await voidModelService.initializeModel(uri)
if (this.commandBarService.getStreamState(uri) === 'streaming') {
@ -479,6 +503,15 @@ export class ToolsService implements IToolsService {
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
},
rewrite_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.`
: ` No lint errors found.`)
: '')
return `Change successfully made to ${params.uri.fsPath}.${lintErrsString}`
},
run_command: (params, result) => {
const {
resolveReason,

View file

@ -106,17 +106,18 @@ ${tripleTick[1]}`
const replaceTool_description = `\
Output a string of SEARCH/REPLACE block(s) to implement your desired change.
You are encouraged to output multiple changes at once. Here's how to format your blocks:
A string of SEARCH/REPLACE block(s) to apply to the given file.
You are encouraged to output multiple changes in this string when possible. For example:
${searchReplaceBlockTemplate}
1. Don't forget to wrap your output in triple backticks.
Guidelines:
1. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code.
2. The ORIGINAL code in each SEARCH/REPLACE block must EXACTLY match lines in the original file. Do not add or remove any whitespace or comments from the original code.
2. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible.
3. Each ORIGINAL text must be large enough to uniquely identify the change in the file. However, bias towards writing as little as possible.
3. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.
4. Each ORIGINAL text must be DISJOINT from all other ORIGINAL text.`
4. This field is a STRING (not an array). You should wrap the string in triple backticks.`
// ======================================================== tools ========================================================
@ -285,15 +286,25 @@ export const voidTools = {
},
},
edit_file: { // APPLY TOOL
edit_file: {
name: 'edit_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.`,
description: `Edit the contents of a file. You must provide the file's URI as well as a SINGLE string of SEARCH/REPLACE block(s) that will be used to apply the edit.`,
params: {
...uriParam('file'),
search_replace_blocks: { description: replaceTool_description }
},
},
rewrite_file: {
name: 'rewrite_file',
description: `Edits a file, deleting all the old contents and replacing them with your new contents. Use this tool if you want to edit a file you just created.`,
params: {
...uriParam('file'),
new_content: { description: `The new contents of the file.` }
},
},
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.`,

View file

@ -19,6 +19,7 @@ export type ShallowDirectoryItem = {
export const approvalTypeOfToolName: Partial<{ [T in ToolName]?: 'edits' | 'terminal' }> = {
'create_file_or_folder': 'edits',
'delete_file_or_folder': 'edits',
'rewrite_file': 'edits',
'edit_file': 'edits',
'run_command': 'terminal',
}
@ -42,6 +43,7 @@ export type ToolCallParams = {
'search_in_file': { uri: URI, query: string, isRegex: boolean },
'read_lint_errors': { uri: URI },
// ---
'rewrite_file': { uri: URI, newContent: string },
'edit_file': { uri: URI, searchReplaceBlocks: string },
'create_file_or_folder': { uri: URI, isFolder: boolean },
'delete_file_or_folder': { uri: URI, isRecursive: boolean, isFolder: boolean },
@ -61,6 +63,7 @@ export type ToolResultType = {
'search_in_file': { lines: number[]; },
'read_lint_errors': { lintErrors: LintErrorItem[] | null },
// ---
'rewrite_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
'edit_file': Promise<{ lintErrors: LintErrorItem[] | null }>,
'create_file_or_folder': {},
'delete_file_or_folder': {},