mirror of
https://github.com/voideditor/void
synced 2026-05-23 17:38:23 +00:00
rewrite file tool
This commit is contained in:
parent
0bb948ee2d
commit
039d5cb812
7 changed files with 183 additions and 93 deletions
|
|
@ -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
|
||||
|
||||
|
|
|
|||
|
|
@ -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] || []) {
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
|
||||
|
|
|
|||
|
|
@ -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}
|
||||
/>
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
|
|
|
|||
|
|
@ -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': {},
|
||||
|
|
|
|||
Loading…
Reference in a new issue