diff options
Diffstat (limited to 'packages/core/src/tools/modifiable-tool.ts')
| -rw-r--r-- | packages/core/src/tools/modifiable-tool.ts | 163 |
1 files changed, 163 insertions, 0 deletions
diff --git a/packages/core/src/tools/modifiable-tool.ts b/packages/core/src/tools/modifiable-tool.ts new file mode 100644 index 00000000..96fe176c --- /dev/null +++ b/packages/core/src/tools/modifiable-tool.ts @@ -0,0 +1,163 @@ +/** + * @license + * Copyright 2025 Google LLC + * SPDX-License-Identifier: Apache-2.0 + */ + +import { EditorType } from '../utils/editor.js'; +import os from 'os'; +import path from 'path'; +import fs from 'fs'; +import * as Diff from 'diff'; +import { openDiff } from '../utils/editor.js'; +import { DEFAULT_DIFF_OPTIONS } from './diffOptions.js'; +import { isNodeError } from '../utils/errors.js'; +import { Tool } from './tools.js'; + +/** + * A tool that supports a modify operation. + */ +export interface ModifiableTool<ToolParams> extends Tool<ToolParams> { + getModifyContext(abortSignal: AbortSignal): ModifyContext<ToolParams>; +} + +export interface ModifyContext<ToolParams> { + getFilePath: (params: ToolParams) => string; + + getCurrentContent: (params: ToolParams) => Promise<string>; + + getProposedContent: (params: ToolParams) => Promise<string>; + + createUpdatedParams: ( + modifiedProposedContent: string, + originalParams: ToolParams, + ) => ToolParams; +} + +export interface ModifyResult<ToolParams> { + updatedParams: ToolParams; + updatedDiff: string; +} + +export function isModifiableTool<TParams>( + tool: Tool<TParams>, +): tool is ModifiableTool<TParams> { + return 'getModifyContext' in tool; +} + +function createTempFilesForModify( + currentContent: string, + proposedContent: string, + file_path: string, +): { oldPath: string; newPath: string } { + const tempDir = os.tmpdir(); + const diffDir = path.join(tempDir, 'gemini-cli-tool-modify-diffs'); + + if (!fs.existsSync(diffDir)) { + fs.mkdirSync(diffDir, { recursive: true }); + } + + const fileName = path.basename(file_path); + const timestamp = Date.now(); + const tempOldPath = path.join( + diffDir, + `gemini-cli-modify-${fileName}-old-${timestamp}`, + ); + const tempNewPath = path.join( + diffDir, + `gemini-cli-modify-${fileName}-new-${timestamp}`, + ); + + fs.writeFileSync(tempOldPath, currentContent, 'utf8'); + fs.writeFileSync(tempNewPath, proposedContent, 'utf8'); + + return { oldPath: tempOldPath, newPath: tempNewPath }; +} + +function getUpdatedParams<ToolParams>( + tmpOldPath: string, + tempNewPath: string, + originalParams: ToolParams, + modifyContext: ModifyContext<ToolParams>, +): { updatedParams: ToolParams; updatedDiff: string } { + let oldContent = ''; + let newContent = ''; + + try { + oldContent = fs.readFileSync(tmpOldPath, 'utf8'); + } catch (err) { + if (!isNodeError(err) || err.code !== 'ENOENT') throw err; + oldContent = ''; + } + + try { + newContent = fs.readFileSync(tempNewPath, 'utf8'); + } catch (err) { + if (!isNodeError(err) || err.code !== 'ENOENT') throw err; + newContent = ''; + } + + const updatedParams = modifyContext.createUpdatedParams( + newContent, + originalParams, + ); + const updatedDiff = Diff.createPatch( + path.basename(modifyContext.getFilePath(originalParams)), + oldContent, + newContent, + 'Current', + 'Proposed', + DEFAULT_DIFF_OPTIONS, + ); + + return { updatedParams, updatedDiff }; +} + +function deleteTempFiles(oldPath: string, newPath: string): void { + try { + fs.unlinkSync(oldPath); + } catch { + console.error(`Error deleting temp diff file: ${oldPath}`); + } + + try { + fs.unlinkSync(newPath); + } catch { + console.error(`Error deleting temp diff file: ${newPath}`); + } +} + +/** + * Triggers an external editor for the user to modify the proposed content, + * and returns the updated tool parameters and the diff after the user has modified the proposed content. + */ +export async function modifyWithEditor<ToolParams>( + originalParams: ToolParams, + modifyContext: ModifyContext<ToolParams>, + editorType: EditorType, + _abortSignal: AbortSignal, +): Promise<ModifyResult<ToolParams>> { + const currentContent = await modifyContext.getCurrentContent(originalParams); + const proposedContent = + await modifyContext.getProposedContent(originalParams); + + const { oldPath, newPath } = createTempFilesForModify( + currentContent, + proposedContent, + modifyContext.getFilePath(originalParams), + ); + + try { + await openDiff(oldPath, newPath, editorType); + const result = getUpdatedParams( + oldPath, + newPath, + originalParams, + modifyContext, + ); + + return result; + } finally { + deleteTempFiles(oldPath, newPath); + } +} |
