77//! OpenCode ↔ CodexMonitor translation happens here in Rust.
88
99use serde_json:: { json, Value } ;
10- use similar:: TextDiff ;
1110use std:: collections:: HashMap ;
1211use std:: sync:: atomic:: { AtomicU64 , Ordering } ;
1312
13+ use crate :: shared:: diff_utils:: generate_edit_diff;
14+
1415/// Per-session turn state — tracks active turn and item IDs for a single session.
1516#[ derive( Default ) ]
1617struct PerSessionTurnState {
@@ -282,7 +283,9 @@ fn unseen_suffix<'a>(full_text: &'a str, emitted_len: usize) -> Option<&'a str>
282283 if full_text. len ( ) <= emitted_len {
283284 return None ;
284285 }
285- full_text. get ( emitted_len..) . filter ( |suffix| !suffix. is_empty ( ) )
286+ full_text
287+ . get ( emitted_len..)
288+ . filter ( |suffix| !suffix. is_empty ( ) )
286289}
287290
288291fn parse_u64 ( value : Option < & Value > ) -> Option < u64 > {
@@ -573,7 +576,10 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
573576 // aren't injected by send_user_message_core).
574577 let effective_text_owned;
575578 let effective_text = if delta. is_empty ( ) {
576- let full_text = part. get ( "text" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or_default ( ) ;
579+ let full_text = part
580+ . get ( "text" )
581+ . and_then ( |v| v. as_str ( ) )
582+ . unwrap_or_default ( ) ;
577583 let current_user_text = state
578584 . get_turn_state ( & thread_id)
579585 . map ( |ts| ts. user_message_text . clone ( ) )
@@ -620,7 +626,10 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
620626 }
621627 }
622628 let effective_delta = if delta. is_empty ( ) {
623- let full_text = part. get ( "text" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or_default ( ) ;
629+ let full_text = part
630+ . get ( "text" )
631+ . and_then ( |v| v. as_str ( ) )
632+ . unwrap_or_default ( ) ;
624633 let emitted_len = state
625634 . get_turn_state ( & thread_id)
626635 . map ( |ts| ts. agent_message_text_len )
@@ -643,7 +652,8 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
643652 . unwrap_or ( effective_delta. len ( ) ) ;
644653 state. get_turn_state_mut ( & thread_id) . agent_message_text_len = full_len;
645654 } else {
646- state. get_turn_state_mut ( & thread_id) . agent_message_text_len += effective_delta. len ( ) ;
655+ state. get_turn_state_mut ( & thread_id) . agent_message_text_len +=
656+ effective_delta. len ( ) ;
647657 }
648658
649659 let item_id = state. agent_message_item ( & thread_id) ;
@@ -670,7 +680,10 @@ fn translate_part_updated(properties: &Value, state: &mut SessionTranslationStat
670680 }
671681 }
672682 let effective_delta = if delta. is_empty ( ) {
673- let full_text = part. get ( "text" ) . and_then ( |v| v. as_str ( ) ) . unwrap_or_default ( ) ;
683+ let full_text = part
684+ . get ( "text" )
685+ . and_then ( |v| v. as_str ( ) )
686+ . unwrap_or_default ( ) ;
674687 let emitted_len = state
675688 . get_turn_state ( & thread_id)
676689 . map ( |ts| ts. reasoning_text_len )
@@ -1612,10 +1625,7 @@ fn translate_question_completed(properties: &Value) -> Vec<Value> {
16121625
16131626/// Translate an OpenCode `todo.updated` SSE event into a `turn/plan/updated`
16141627/// event so the PlanPanel sidebar reflects the current session todo list.
1615- fn translate_todo_updated (
1616- properties : & Value ,
1617- state : & mut SessionTranslationState ,
1618- ) -> Vec < Value > {
1628+ fn translate_todo_updated ( properties : & Value , state : & mut SessionTranslationState ) -> Vec < Value > {
16191629 let session_id = properties
16201630 . get ( "sessionID" )
16211631 . or_else ( || properties. get ( "sessionId" ) )
@@ -1850,36 +1860,6 @@ fn file_path_from_raw_input(raw_input: &Value) -> Option<String> {
18501860 . map ( ToOwned :: to_owned)
18511861}
18521862
1853- /// Generate a unified diff from oldString/newString edit input.
1854- /// Returns a unified diff string with @@ hunk headers that the frontend can render.
1855- fn generate_edit_diff ( raw_input : & Value , file_path : & str ) -> Option < String > {
1856- let old_string = raw_input. get ( "oldString" ) . and_then ( |v| v. as_str ( ) ) ?;
1857- let new_string = raw_input. get ( "newString" ) . and_then ( |v| v. as_str ( ) ) ?;
1858-
1859- // Don't generate diff for empty old/new (pure create or delete)
1860- if old_string. is_empty ( ) && new_string. is_empty ( ) {
1861- return None ;
1862- }
1863-
1864- let diff = TextDiff :: from_lines ( old_string, new_string) ;
1865- let mut output = String :: new ( ) ;
1866-
1867- // Add file header
1868- output. push_str ( & format ! ( "--- a/{file_path}\n " ) ) ;
1869- output. push_str ( & format ! ( "+++ b/{file_path}\n " ) ) ;
1870-
1871- // Generate unified diff hunks
1872- for hunk in diff. unified_diff ( ) . context_radius ( 3 ) . iter_hunks ( ) {
1873- output. push_str ( & hunk. to_string ( ) ) ;
1874- }
1875-
1876- if output. contains ( "@@" ) {
1877- Some ( output)
1878- } else {
1879- None
1880- }
1881- }
1882-
18831863fn build_tool_item (
18841864 item_id : & str ,
18851865 item_type : & str ,
@@ -2524,7 +2504,10 @@ mod tests {
25242504 } ) ;
25252505 let events1 = translate_sse_event ( & delta1, & mut state) ;
25262506 assert_eq ! ( events1. len( ) , 1 ) ;
2527- assert_eq ! ( events1[ 0 ] [ "params" ] [ "delta" ] , "I'm currently in Plan Mode (read-only), " ) ;
2507+ assert_eq ! (
2508+ events1[ 0 ] [ "params" ] [ "delta" ] ,
2509+ "I'm currently in Plan Mode (read-only), "
2510+ ) ;
25282511
25292512 let delta2 = json ! ( {
25302513 "type" : "message.part.delta" ,
0 commit comments