Enable "modify" in write tool (#1044)

This commit is contained in:
Leo
2025-06-14 19:20:04 +01:00
committed by GitHub
parent 93909a2dd3
commit 2c6aae863a
6 changed files with 626 additions and 287 deletions

View File

@@ -6,7 +6,6 @@
import * as fs from 'fs';
import * as path from 'path';
import * as os from 'os';
import * as Diff from 'diff';
import {
BaseTool,
@@ -23,9 +22,8 @@ import { GeminiClient } from '../core/client.js';
import { Config, ApprovalMode } from '../config/config.js';
import { ensureCorrectEdit } from '../utils/editCorrector.js';
import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js';
import { openDiff } from '../utils/editor.js';
import { ReadFileTool } from './read-file.js';
import { EditorType } from '../utils/editor.js';
import { ModifiableTool, ModifyContext } from './modifiable-tool.js';
/**
* Parameters for the Edit tool
@@ -64,13 +62,14 @@ interface CalculatedEdit {
/**
* Implementation of the Edit tool logic
*/
export class EditTool extends BaseTool<EditToolParams, ToolResult> {
export class EditTool
extends BaseTool<EditToolParams, ToolResult>
implements ModifiableTool<EditToolParams>
{
static readonly Name = 'replace';
private readonly config: Config;
private readonly rootDirectory: string;
private readonly client: GeminiClient;
private tempOldDiffPath?: string;
private tempNewDiffPath?: string;
/**
* Creates a new instance of the EditLogic
@@ -432,132 +431,6 @@ Expectation for required parameters:
}
}
/**
* Creates temp files for the current and proposed file contents and opens a diff tool.
* When the diff tool is closed, the tool will check if the file has been modified and provide the updated params.
* @returns Updated params and diff if the file has been modified, undefined otherwise.
*/
async onModify(
params: EditToolParams,
_abortSignal: AbortSignal,
editorType: EditorType,
): Promise<
{ updatedParams: EditToolParams; updatedDiff: string } | undefined
> {
const { oldPath, newPath } = this.createTempFiles(params);
this.tempOldDiffPath = oldPath;
this.tempNewDiffPath = newPath;
await openDiff(this.tempOldDiffPath, this.tempNewDiffPath, editorType);
return await this.getUpdatedParamsIfModified(params, _abortSignal);
}
private async getUpdatedParamsIfModified(
params: EditToolParams,
_abortSignal: AbortSignal,
): Promise<
{ updatedParams: EditToolParams; updatedDiff: string } | undefined
> {
if (!this.tempOldDiffPath || !this.tempNewDiffPath) return undefined;
let oldContent = '';
let newContent = '';
try {
oldContent = fs.readFileSync(this.tempOldDiffPath, 'utf8');
} catch (err) {
if (!isNodeError(err) || err.code !== 'ENOENT') throw err;
oldContent = '';
}
try {
newContent = fs.readFileSync(this.tempNewDiffPath, 'utf8');
} catch (err) {
if (!isNodeError(err) || err.code !== 'ENOENT') throw err;
newContent = '';
}
// Combine the edits into a single edit
const updatedParams: EditToolParams = {
...params,
old_string: oldContent,
new_string: newContent,
};
const updatedDiff = Diff.createPatch(
path.basename(params.file_path),
oldContent,
newContent,
'Current',
'Proposed',
DEFAULT_DIFF_OPTIONS,
);
this.deleteTempFiles();
return { updatedParams, updatedDiff };
}
private createTempFiles(params: EditToolParams): Record<string, string> {
this.deleteTempFiles();
const tempDir = os.tmpdir();
const diffDir = path.join(tempDir, 'gemini-cli-edit-tool-diffs');
if (!fs.existsSync(diffDir)) {
fs.mkdirSync(diffDir, { recursive: true });
}
const fileName = path.basename(params.file_path);
const timestamp = Date.now();
const tempOldPath = path.join(
diffDir,
`gemini-cli-edit-${fileName}-old-${timestamp}`,
);
const tempNewPath = path.join(
diffDir,
`gemini-cli-edit-${fileName}-new-${timestamp}`,
);
let currentContent = '';
try {
currentContent = fs.readFileSync(params.file_path, 'utf8');
} catch (err) {
if (!isNodeError(err) || err.code !== 'ENOENT') throw err;
currentContent = '';
}
let proposedContent = currentContent;
proposedContent = this._applyReplacement(
proposedContent,
params.old_string,
params.new_string,
params.old_string === '' && currentContent === '',
);
fs.writeFileSync(tempOldPath, currentContent, 'utf8');
fs.writeFileSync(tempNewPath, proposedContent, 'utf8');
return {
oldPath: tempOldPath,
newPath: tempNewPath,
};
}
private deleteTempFiles(): void {
try {
if (this.tempOldDiffPath) {
fs.unlinkSync(this.tempOldDiffPath);
this.tempOldDiffPath = undefined;
}
} catch {
console.error(`Error deleting temp diff file: `, this.tempOldDiffPath);
}
try {
if (this.tempNewDiffPath) {
fs.unlinkSync(this.tempNewDiffPath);
this.tempNewDiffPath = undefined;
}
} catch {
console.error(`Error deleting temp diff file: `, this.tempNewDiffPath);
}
}
/**
* Creates parent directories if they don't exist
*/
@@ -567,4 +440,39 @@ Expectation for required parameters:
fs.mkdirSync(dirName, { recursive: true });
}
}
getModifyContext(_: AbortSignal): ModifyContext<EditToolParams> {
return {
getFilePath: (params: EditToolParams) => params.file_path,
getCurrentContent: async (params: EditToolParams): Promise<string> => {
try {
return fs.readFileSync(params.file_path, 'utf8');
} catch (err) {
if (!isNodeError(err) || err.code !== 'ENOENT') throw err;
return '';
}
},
getProposedContent: async (params: EditToolParams): Promise<string> => {
try {
const currentContent = fs.readFileSync(params.file_path, 'utf8');
return this._applyReplacement(
currentContent,
params.old_string,
params.new_string,
params.old_string === '' && currentContent === '',
);
} catch (err) {
if (!isNodeError(err) || err.code !== 'ENOENT') throw err;
return '';
}
},
createUpdatedParams: (
modifiedProposedContent: string,
originalParams: EditToolParams,
): EditToolParams => ({
...originalParams,
new_string: modifiedProposedContent,
}),
};
}
}