11import type { ToolCallContent } from "@agentclientprotocol/sdk" ;
2- import { applyPatch } from "diff" ;
2+ import { applyPatch , parsePatch } from "diff" ;
33import { readFile } from "node:fs/promises" ;
44import path from "node:path" ;
55import type { UpdateSessionEvent } from "./ACPSessionConnection" ;
@@ -86,6 +86,7 @@ export async function createMcpToolCallUpdate(
8686) : Promise < UpdateSessionEvent > {
8787 return createExecuteToolCallUpdate ( item , `mcp.${ item . server } .${ item . tool } ` ) ;
8888}
89+
8990export async function createDynamicToolCallUpdate (
9091 item : ThreadItem & { type : "dynamicToolCall" }
9192) : Promise < UpdateSessionEvent > {
@@ -235,7 +236,28 @@ async function createPatchContent(change: FileUpdateChange): Promise<ToolCallCon
235236 } ;
236237 }
237238
238- const oldContent = change . kind . type === "add" ? "" : await readFile ( change . path , { encoding : "utf8" } ) ;
239+ if ( change . kind . type === "delete" ) {
240+ // If the patch deletes a file, the old content may be only available from the diff.
241+ const oldContent = await readFile ( change . path , { encoding : "utf8" } ) . catch ( ( ) =>
242+ isUnifiedDiff ( change . diff ) ? patchToDeletedContent ( change . diff ) : change . diff
243+ ) ;
244+
245+ return {
246+ type : "diff" ,
247+ oldText : oldContent ,
248+ newText : "" ,
249+ path : change . path ,
250+ _meta : {
251+ kind : "delete" ,
252+ }
253+ }
254+ }
255+
256+ const oldContent = change . kind . type === "add" ? "" : await readFile ( change . path , { encoding : "utf8" } ) . catch ( ( ) => null ) ;
257+ if ( oldContent === null ) {
258+ return null ;
259+ }
260+
239261 const newContent = applyPatch ( oldContent , change . diff ) ;
240262 if ( newContent === false ) {
241263 return null ;
@@ -254,3 +276,40 @@ async function createPatchContent(change: FileUpdateChange): Promise<ToolCallCon
254276function isUnifiedDiff ( content : string ) : boolean {
255277 return content . startsWith ( "--- " ) || content . includes ( "\n--- " ) ;
256278}
279+
280+ /**
281+ * Recreates the content of a deleted file from the unified diff.
282+ * @param unifiedDiff The unified diff of the file deletion patch
283+ */
284+ function patchToDeletedContent ( unifiedDiff : string ) : string | null {
285+ try {
286+ const [ patch ] = parsePatch ( unifiedDiff ) ;
287+ if ( ! patch || patch . hunks . length === 0 ) {
288+ return null ;
289+ }
290+
291+ const oldLines : string [ ] = [ ] ;
292+ let hasNoTrailingNewlineMarker = false ;
293+
294+ for ( const hunk of patch . hunks ) {
295+ for ( const line of hunk . lines ) {
296+ if ( line === "\\ No newline at end of file" ) {
297+ hasNoTrailingNewlineMarker = true ;
298+ continue ;
299+ }
300+ if ( line . startsWith ( "-" ) || line . startsWith ( " " ) ) {
301+ oldLines . push ( line . slice ( 1 ) ) ;
302+ }
303+ }
304+ }
305+
306+ if ( oldLines . length === 0 ) {
307+ return "" ;
308+ }
309+
310+ const oldText = oldLines . join ( "\n" ) ;
311+ return hasNoTrailingNewlineMarker || ! unifiedDiff . endsWith ( "\n" ) ? oldText : `${ oldText } \n` ;
312+ } catch {
313+ return null ;
314+ }
315+ }
0 commit comments