diff --git a/src/vs/workbench/contrib/void/browser/chatThreadService.ts b/src/vs/workbench/contrib/void/browser/chatThreadService.ts index baae23e4..b3aa1651 100644 --- a/src/vs/workbench/contrib/void/browser/chatThreadService.ts +++ b/src/vs/workbench/contrib/void/browser/chatThreadService.ts @@ -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 diff --git a/src/vs/workbench/contrib/void/browser/editCodeService.ts b/src/vs/workbench/contrib/void/browser/editCodeService.ts index d9d0d9f3..c42d3139 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeService.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeService.ts @@ -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] || []) { diff --git a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts index 5ad58a33..26ff9d2b 100644 --- a/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts +++ b/src/vs/workbench/contrib/void/browser/editCodeServiceInterface.ts @@ -45,6 +45,7 @@ export interface IEditCodeService { callBeforeStartApplying(opts: CallBeforeStartApplyingOpts): Promise; startApplying(opts: StartApplyingOpts): [URI, Promise] | 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; 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 7e34f9fd..5bd3e2fc 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 @@ -808,6 +808,84 @@ const ToolHeaderWrapper = ({ +const EditTool = ({ toolMessage, threadId, messageIdx, content }: Parameters>[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 = + + + componentParams.desc2 = + } + 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 = + } + + // add children + if (toolMessage.type !== 'tool_error') { + const { result } = toolMessage + + componentParams.bottomChildren = + + componentParams.children = + + + } + else { + // error + const { result } = toolMessage + if (params) { + componentParams.children = + {/* error */} + + {result} + + + {/* content */} + + + } + else { + componentParams.children = + {result} + + } + } + } + + return +} 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
- +
} @@ -1977,84 +2063,14 @@ const toolNameToComponent: { [T in ToolName]: { resultWrapper: ResultWrapper, return } }, + 'rewrite_file': { + resultWrapper: (params) => { + return + } + }, '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 = - - - componentParams.desc2 = - } - 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 = - } - - // add children - if (toolMessage.type !== 'tool_error') { - const { result } = toolMessage - - componentParams.bottomChildren = - - componentParams.children = - - - } - else { - // error - const { result } = toolMessage - if (params) { - componentParams.children = - {/* error */} - - {result} - - - {/* content */} - - - } - else { - componentParams.children = - {result} - - } - } - } - - return + resultWrapper: (params) => { + return } }, @@ -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 = @@ -2653,7 +2669,7 @@ const EditToolSoFar = ({ toolCallSoFar, }: { toolCallSoFar: RawToolCallObj }) => > @@ -2794,7 +2810,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 9a55d134..6987e873 100644 --- a/src/vs/workbench/contrib/void/browser/toolsService.ts +++ b/src/vs/workbench/contrib/void/browser/toolsService.ts @@ -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, diff --git a/src/vs/workbench/contrib/void/common/prompt/prompts.ts b/src/vs/workbench/contrib/void/common/prompt/prompts.ts index acc8ecb1..f8c7fc11 100644 --- a/src/vs/workbench/contrib/void/common/prompt/prompts.ts +++ b/src/vs/workbench/contrib/void/common/prompt/prompts.ts @@ -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.`, diff --git a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts index 016344da..0f48992f 100644 --- a/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts +++ b/src/vs/workbench/contrib/void/common/toolsServiceTypes.ts @@ -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': {},