/** * @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 extends Tool { getModifyContext(abortSignal: AbortSignal): ModifyContext; } export interface ModifyContext { getFilePath: (params: ToolParams) => string; getCurrentContent: (params: ToolParams) => Promise; getProposedContent: (params: ToolParams) => Promise; createUpdatedParams: ( oldContent: string, modifiedProposedContent: string, originalParams: ToolParams, ) => ToolParams; } export interface ModifyResult { updatedParams: ToolParams; updatedDiff: string; } export function isModifiableTool( tool: Tool, ): tool is ModifiableTool { 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( tmpOldPath: string, tempNewPath: string, originalParams: ToolParams, modifyContext: ModifyContext, ): { 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( oldContent, 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( originalParams: ToolParams, modifyContext: ModifyContext, editorType: EditorType, _abortSignal: AbortSignal, ): Promise> { 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); } }