@@ -10,7 +10,7 @@ use serde_json::{json, Value};
1010use std:: collections:: HashMap ;
1111use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
1212
13- use crate :: shared:: diff_utils:: generate_edit_diff;
13+ use crate :: shared:: diff_utils:: { generate_apply_patch_changes , generate_edit_diff} ;
1414
1515/// Per-session turn state — tracks active turn and item IDs for a single session.
1616#[ derive( Default ) ]
@@ -1052,7 +1052,7 @@ fn translate_tool_part(
10521052 } ) ) ;
10531053
10541054 if let Some ( ref input) = raw_input {
1055- let delta_text = serde_json :: to_string_pretty ( input) . unwrap_or_default ( ) ;
1055+ let delta_text = tool_input_delta_text ( tool_name , input) ;
10561056 if !delta_text. is_empty ( ) {
10571057 let method = if item_type == "fileChange" {
10581058 "item/fileChange/outputDelta"
@@ -1122,6 +1122,55 @@ fn translate_tool_part(
11221122 events
11231123}
11241124
1125+ fn tool_input_delta_text ( tool_name : & str , raw_input : & Value ) -> String {
1126+ if tool_name == "apply_patch" {
1127+ return build_apply_patch_delta_summary ( raw_input)
1128+ . unwrap_or_else ( || "Applying patch..." . to_string ( ) ) ;
1129+ }
1130+
1131+ serde_json:: to_string_pretty ( raw_input) . unwrap_or_default ( )
1132+ }
1133+
1134+ fn build_apply_patch_delta_summary ( raw_input : & Value ) -> Option < String > {
1135+ let patch_text = raw_input. get ( "patchText" ) . and_then ( |v| v. as_str ( ) ) ?;
1136+ if patch_text. trim ( ) . is_empty ( ) {
1137+ return Some ( "Applying patch..." . to_string ( ) ) ;
1138+ }
1139+
1140+ let changes = generate_apply_patch_changes ( raw_input) ?;
1141+ if changes. is_empty ( ) {
1142+ return Some ( "Applying patch..." . to_string ( ) ) ;
1143+ }
1144+
1145+ let mut labels = Vec :: new ( ) ;
1146+ for change in changes. iter ( ) . take ( 3 ) {
1147+ let kind = change
1148+ . get ( "kind" )
1149+ . and_then ( |v| v. as_str ( ) )
1150+ . unwrap_or ( "modify" ) ;
1151+ let path = change
1152+ . get ( "path" )
1153+ . and_then ( |v| v. as_str ( ) )
1154+ . unwrap_or ( "file" ) ;
1155+ labels. push ( format ! ( "{kind} {path}" ) ) ;
1156+ }
1157+
1158+ let total = changes. len ( ) ;
1159+ let mut summary = format ! (
1160+ "Applying patch to {total} file{}" ,
1161+ if total == 1 { "" } else { "s" }
1162+ ) ;
1163+ if !labels. is_empty ( ) {
1164+ summary. push_str ( ": " ) ;
1165+ summary. push_str ( & labels. join ( ", " ) ) ;
1166+ }
1167+ if total > labels. len ( ) {
1168+ summary. push_str ( & format ! ( ", +{} more" , total - labels. len( ) ) ) ;
1169+ }
1170+
1171+ Some ( summary)
1172+ }
1173+
11251174fn build_explore_entry ( tool_name : & str , title : & str , raw_input : Option < & Value > ) -> Value {
11261175 let kind = tool_to_explore_kind ( tool_name) ;
11271176
@@ -1731,7 +1780,7 @@ pub(crate) fn build_agent_message_completed(
17311780
17321781fn tool_kind_to_item_type ( kind : & str ) -> & str {
17331782 match kind {
1734- "edit" | "write" | "create" => "fileChange" ,
1783+ "edit" | "write" | "create" | "apply_patch" => "fileChange" ,
17351784 "bash" | "command" | "terminal" => "commandExecution" ,
17361785 "read" | "grep" | "glob" | "list" | "ls" => "explore" ,
17371786 "todowrite" => "todowrite" ,
@@ -1935,7 +1984,9 @@ fn build_tool_item(
19351984 let mut changes = changes_from_content. unwrap_or_default ( ) ;
19361985 if changes. is_empty ( ) {
19371986 if let Some ( input) = raw_input {
1938- if let Some ( path) = file_path_from_raw_input ( input) {
1987+ if let Some ( parsed_changes) = generate_apply_patch_changes ( input) {
1988+ changes = parsed_changes;
1989+ } else if let Some ( path) = file_path_from_raw_input ( input) {
19391990 let mut change = json ! ( { "path" : path, "kind" : "modify" } ) ;
19401991 if let Some ( diff) = generate_edit_diff ( input, & path) {
19411992 change[ "diff" ] = json ! ( diff) ;
@@ -2123,6 +2174,90 @@ mod tests {
21232174 assert ! ( diff. contains( "+ println!(\" Hello, world!\" );" ) ) ;
21242175 }
21252176
2177+ #[ test]
2178+ fn apply_patch_tool_generates_file_change_entries ( ) {
2179+ let mut state = make_state ( ) ;
2180+ let completed = json ! ( {
2181+ "type" : "message.part.updated" ,
2182+ "properties" : {
2183+ "part" : {
2184+ "type" : "tool" ,
2185+ "id" : "tc_apply_patch" ,
2186+ "tool" : "apply_patch" ,
2187+ "state" : {
2188+ "status" : "completed" ,
2189+ "input" : {
2190+ "patchText" : "*** Begin Patch\n *** Update File: src/main.rs\n @@ -1,1 +1,1 @@\n -old\n +new\n *** Add File: notes.txt\n +hello\n *** Delete File: old.txt\n *** Update File: src/from.rs\n *** Move to: src/to.rs\n @@ -1,1 +1,1 @@\n -before\n +after\n *** End Patch"
2191+ } ,
2192+ "output" : "Patch applied successfully."
2193+ }
2194+ }
2195+ }
2196+ } ) ;
2197+
2198+ let events = translate_sse_event ( & completed, & mut state) ;
2199+ assert_eq ! ( events. len( ) , 1 ) ;
2200+ let item = & events[ 0 ] [ "params" ] [ "item" ] ;
2201+ assert_eq ! ( item[ "type" ] , "fileChange" ) ;
2202+
2203+ let changes = item[ "changes" ] . as_array ( ) . expect ( "changes should be array" ) ;
2204+ assert_eq ! ( changes. len( ) , 4 ) ;
2205+ assert_eq ! ( changes[ 0 ] [ "path" ] , "src/main.rs" ) ;
2206+ assert_eq ! ( changes[ 0 ] [ "kind" ] , "modify" ) ;
2207+ assert ! ( changes[ 0 ] [ "diff" ]
2208+ . as_str( )
2209+ . expect( "modify diff" )
2210+ . contains( "--- a/src/main.rs" ) ) ;
2211+
2212+ assert_eq ! ( changes[ 1 ] [ "path" ] , "notes.txt" ) ;
2213+ assert_eq ! ( changes[ 1 ] [ "kind" ] , "add" ) ;
2214+ assert ! ( changes[ 1 ] [ "diff" ]
2215+ . as_str( )
2216+ . expect( "add diff" )
2217+ . contains( "--- /dev/null" ) ) ;
2218+
2219+ assert_eq ! ( changes[ 2 ] [ "path" ] , "old.txt" ) ;
2220+ assert_eq ! ( changes[ 2 ] [ "kind" ] , "delete" ) ;
2221+ assert ! ( changes[ 2 ] . get( "diff" ) . is_none( ) ) ;
2222+
2223+ assert_eq ! ( changes[ 3 ] [ "path" ] , "src/to.rs" ) ;
2224+ let rename_diff = changes[ 3 ] [ "diff" ] . as_str ( ) . expect ( "rename diff" ) ;
2225+ assert ! ( rename_diff. contains( "--- a/src/from.rs" ) ) ;
2226+ assert ! ( rename_diff. contains( "+++ b/src/to.rs" ) ) ;
2227+ }
2228+
2229+ #[ test]
2230+ fn apply_patch_running_delta_uses_summary_not_patch_text ( ) {
2231+ let mut state = make_state ( ) ;
2232+ let running = json ! ( {
2233+ "type" : "message.part.updated" ,
2234+ "properties" : {
2235+ "part" : {
2236+ "type" : "tool" ,
2237+ "id" : "tc_apply_patch_running" ,
2238+ "tool" : "apply_patch" ,
2239+ "state" : {
2240+ "status" : "running" ,
2241+ "input" : {
2242+ "patchText" : "*** Begin Patch\n *** Update File: src/main.rs\n @@ -1,1 +1,1 @@\n -old\n +new\n *** Add File: notes.txt\n +hello\n *** End Patch"
2243+ }
2244+ }
2245+ }
2246+ }
2247+ } ) ;
2248+
2249+ let events = translate_sse_event ( & running, & mut state) ;
2250+ assert_eq ! ( events. len( ) , 2 ) ;
2251+ assert_eq ! ( events[ 0 ] [ "method" ] , "item/started" ) ;
2252+ assert_eq ! ( events[ 1 ] [ "method" ] , "item/fileChange/outputDelta" ) ;
2253+ let delta = events[ 1 ] [ "params" ] [ "delta" ] . as_str ( ) . expect ( "delta string" ) ;
2254+ assert ! ( delta. contains( "Applying patch to 2 files" ) ) ;
2255+ assert ! ( delta. contains( "modify src/main.rs" ) ) ;
2256+ assert ! ( delta. contains( "add notes.txt" ) ) ;
2257+ assert ! ( !delta. contains( "*** Begin Patch" ) ) ;
2258+ assert ! ( !delta. contains( "patchText" ) ) ;
2259+ }
2260+
21262261 #[ test]
21272262 fn text_after_tool_completion_uses_new_agent_message_item ( ) {
21282263 let mut state = make_state ( ) ;
0 commit comments