From b00e89795ecfee8347fc528a531d7c0132bcf5ba Mon Sep 17 00:00:00 2001 From: iris <2+iris@noreply.localhost> Date: Wed, 9 Sep 2026 21:30:15 -0400 Subject: [PATCH] Parse Codex app-server patch payloads --- server/src/session/codex/translate.rs | 91 ++++++++++++++++++--------- 1 file changed, 63 insertions(+), 28 deletions(-) diff --git a/server/src/session/codex/translate.rs b/server/src/session/codex/translate.rs index e61aedc..9445e25 100644 --- a/server/src/session/codex/translate.rs +++ b/server/src/session/codex/translate.rs @@ -305,34 +305,58 @@ fn shell_word(word: &Value) -> String { } fn file_change_diff(item: &Value) -> String { - let Some(changes) = item.get("changes").and_then(Value::as_object) else { - return String::new(); - }; - changes - .iter() - .map(|(path, change)| { - let kind = change.get("type").and_then(Value::as_str); - let from = if kind == Some("add") { - "/dev/null" - } else { - path - }; - let to = if kind == Some("delete") { - "/dev/null" - } else { - change - .get("move_path") + match item.get("changes") { + // Current app-server protocol: [{path, kind: {type, move_path?}, diff}]. + Some(Value::Array(changes)) => changes + .iter() + .filter_map(|change| { + let path = change.get("path")?.as_str()?; + let kind = change.pointer("/kind/type").and_then(Value::as_str); + let moved = change.pointer("/kind/move_path").and_then(Value::as_str); + let body = change + .get("diff") .and_then(Value::as_str) - .unwrap_or(path) - }; - let body = change - .get("unified_diff") - .and_then(Value::as_str) - .unwrap_or_default(); - format!("--- {from}\n+++ {to}\n{body}") - }) - .collect::>() - .join("\n") + .unwrap_or_default(); + Some(render_file_diff(path, kind, moved, body)) + }) + .collect::>() + .join("\n"), + // Older exec protocol and durable Codex rollouts: {path: {type, unified_diff, ...}}. + Some(Value::Object(changes)) => changes + .iter() + .map(|(path, change)| { + render_file_diff( + path, + change.get("type").and_then(Value::as_str), + change.get("move_path").and_then(Value::as_str), + change + .get("unified_diff") + .and_then(Value::as_str) + .unwrap_or_default(), + ) + }) + .collect::>() + .join("\n"), + _ => String::new(), + } +} + +fn render_file_diff(path: &str, kind: Option<&str>, moved: Option<&str>, body: &str) -> String { + // Some protocol revisions carry complete unified diffs and some carry only their hunks. + if body.starts_with("--- ") || body.starts_with("diff --git ") { + return body.to_string(); + } + let from = if kind == Some("add") { + "/dev/null" + } else { + path + }; + let to = if kind == Some("delete") { + "/dev/null" + } else { + moved.unwrap_or(path) + }; + format!("--- {from}\n+++ {to}\n{body}") } fn tool_output(item: &Value) -> String { @@ -537,7 +561,7 @@ mod tests { assert!(started.is_empty()); assert_eq!( translator.translate(&line( - r#"{"method":"item/completed","params":{"item":{"id":"patch-1","type":"fileChange","changes":{"src/main.rs":{"type":"update","unified_diff":"@@ -1 +1 @@\n-old\n+new\n","move_path":null}},"status":"completed","stdout":"Success"}}}"# + r#"{"method":"item/completed","params":{"item":{"id":"patch-1","type":"fileChange","changes":[{"path":"src/main.rs","kind":{"type":"update","move_path":null},"diff":"@@ -1 +1 @@\n-old\n+new\n"}],"status":"completed","stdout":"Success"}}}"# )), vec![ patch_start( @@ -552,6 +576,17 @@ mod tests { ); } + #[test] + fn the_legacy_file_change_shape_remains_readable() { + let item = line( + r#"{"changes":{"old.rs":{"type":"update","unified_diff":"@@ -1 +1 @@\n-a\n+b\n","move_path":"new.rs"}}}"#, + ); + assert_eq!( + file_change_diff(&item), + "--- old.rs\n+++ new.rs\n@@ -1 +1 @@\n-a\n+b\n" + ); + } + #[test] fn translates_native_app_server_streaming_without_repeating_the_final_item() { let mut translator = Translator::default();