@@ -40,13 +40,26 @@ export async function createFileChangeUpdate(
4040 item : ThreadItem & { type : "fileChange" }
4141) : Promise < UpdateSessionEvent > {
4242 const patches = await createFileChangeContents ( item . changes ) ;
43+ const details = createFileChangeDetails ( item . changes ) ;
4344 return {
4445 sessionUpdate : "tool_call" ,
4546 toolCallId : item . id ,
46- title : "Editing files" ,
47+ title : details . title ,
4748 kind : "edit" ,
4849 status : toAcpStatus ( item . status ) ,
4950 content : patches ,
51+ locations : details . locations ,
52+ rawInput : details . rawInput ,
53+ } ;
54+ }
55+
56+ export function createFileChangeCompletionUpdate (
57+ item : ThreadItem & { type : "fileChange" }
58+ ) : UpdateSessionEvent {
59+ return {
60+ sessionUpdate : "tool_call_update" ,
61+ toolCallId : item . id ,
62+ status : toAcpStatus ( item . status ) ,
5063 } ;
5164}
5265
@@ -60,6 +73,67 @@ export async function createFileChangeContents(changes: Array<FileUpdateChange>)
6073 return patches ;
6174}
6275
76+ export function createFileChangeLocations ( changes : Array < FileUpdateChange > ) : Array < { path : string } > {
77+ return Array . from ( new Set ( changes . map ( change => change . path ) ) ) . map ( path => ( { path } ) ) ;
78+ }
79+
80+ export function createRawFileChangeInput ( changes : Array < FileUpdateChange > ) : {
81+ changes : Array < {
82+ path : string ;
83+ kind : FileUpdateChange [ "kind" ] ;
84+ diff : string ;
85+ } > ;
86+ } {
87+ return {
88+ changes : changes . map ( change => ( {
89+ path : change . path ,
90+ kind : change . kind ,
91+ diff : change . diff ,
92+ } ) ) ,
93+ } ;
94+ }
95+
96+ export function parseUnifiedDiffChanges ( unifiedDiff : string ) : Array < FileUpdateChange > {
97+ try {
98+ return parsePatch ( unifiedDiff )
99+ . map ( patch => {
100+ const oldFileName = normalizeDiffPath ( patch . oldFileName ) ;
101+ const newFileName = normalizeDiffPath ( patch . newFileName ) ;
102+ const path = newFileName === "/dev/null" ? oldFileName : newFileName ;
103+ if ( ! path ) {
104+ return null ;
105+ }
106+ return {
107+ path,
108+ kind : toPatchChangeKind ( oldFileName , newFileName ) ,
109+ diff : formatParsedPatch ( patch , oldFileName , newFileName ) ,
110+ } satisfies FileUpdateChange ;
111+ } )
112+ . filter ( ( change ) : change is FileUpdateChange => change !== null ) ;
113+ } catch {
114+ return [ ] ;
115+ }
116+ }
117+
118+ function createFileChangeDetails ( changes : Array < FileUpdateChange > ) : {
119+ title : string ;
120+ locations : Array < { path : string } > ;
121+ rawInput : {
122+ changes : Array < {
123+ path : string ;
124+ kind : FileUpdateChange [ "kind" ] ;
125+ diff : string ;
126+ } > ;
127+ } ;
128+ } {
129+ const uniquePaths = createFileChangeLocations ( changes ) ;
130+ return {
131+ title : uniquePaths . length > 0 ? uniquePaths . map ( location => location . path ) . join ( ", " ) : "File change" ,
132+ locations : uniquePaths ,
133+ rawInput : createRawFileChangeInput ( changes ) ,
134+ } ;
135+ }
136+
63137export async function createCommandExecutionUpdate (
64138 item : ThreadItem & { type : "commandExecution" }
65139) : Promise < UpdateSessionEvent > {
@@ -313,6 +387,52 @@ function isUnifiedDiff(content: string): boolean {
313387 return content . startsWith ( "--- " ) || content . includes ( "\n--- " ) ;
314388}
315389
390+ function normalizeDiffPath ( fileName : string | undefined ) : string | undefined {
391+ if ( ! fileName || fileName === "/dev/null" ) {
392+ return fileName ;
393+ }
394+ return fileName . replace ( / ^ [ a b ] \/ / , "" ) ;
395+ }
396+
397+ function toPatchChangeKind ( oldFileName : string | undefined , newFileName : string | undefined ) : FileUpdateChange [ "kind" ] {
398+ if ( oldFileName === "/dev/null" ) {
399+ return { type : "add" } ;
400+ }
401+ if ( newFileName === "/dev/null" ) {
402+ return { type : "delete" } ;
403+ }
404+ return {
405+ type : "update" ,
406+ move_path : oldFileName && newFileName && oldFileName !== newFileName ? oldFileName : null ,
407+ } ;
408+ }
409+
410+ function formatParsedPatch (
411+ patch : ReturnType < typeof parsePatch > [ number ] ,
412+ oldFileName : string | undefined ,
413+ newFileName : string | undefined ,
414+ ) : string {
415+ const lines = [
416+ `--- ${ oldFileName ?? "/dev/null" } ` ,
417+ `+++ ${ newFileName ?? "/dev/null" } ` ,
418+ ] ;
419+ for ( const hunk of patch . hunks ) {
420+ lines . push ( `@@ -${ formatHunkRange ( hunk . oldStart , hunk . oldLines ) } +${ formatHunkRange ( hunk . newStart , hunk . newLines ) } @@` ) ;
421+ lines . push ( ...hunk . lines ) ;
422+ }
423+ return lines . join ( "\n" ) ;
424+ }
425+
426+ function formatHunkRange ( start : number , lineCount : number ) : string {
427+ if ( lineCount === 0 ) {
428+ return `${ start - 1 } ,0` ;
429+ }
430+ if ( lineCount === 1 ) {
431+ return `${ start } ` ;
432+ }
433+ return `${ start } ,${ lineCount } ` ;
434+ }
435+
316436/**
317437 * Recreates the content of a deleted file from the unified diff.
318438 * @param unifiedDiff The unified diff of the file deletion patch
0 commit comments