diff --git a/codex-rs/core/src/event_mapping_tests.rs b/codex-rs/core/src/event_mapping_tests.rs index df636d4a6b34..4cf1b72d5d65 100644 --- a/codex-rs/core/src/event_mapping_tests.rs +++ b/codex-rs/core/src/event_mapping_tests.rs @@ -67,7 +67,7 @@ fn parses_user_message_with_text_and_two_images() { #[test] fn skips_local_image_label_text() { let image_url = "data:image/png;base64,abc".to_string(); - let label = codex_protocol::models::local_image_open_tag_text(/*label_number*/ 1); + let label = r#""#.to_string(); let user_text = "Please review this image.".to_string(); let item = ResponseItem::Message { diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 87eb03079e84..7018274510e0 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -161,7 +161,9 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu role: "user".to_string(), content: vec![ ContentItem::InputText { - text: codex_protocol::models::local_image_open_tag_text(/*label_number*/ 1), + text: codex_protocol::models::local_image_open_tag_text_with_path( + /*label_number*/ 1, &abs_path, + ), }, ContentItem::InputImage { image_url, diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 5a08610cd6c0..d95370c0614b 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -1019,9 +1019,10 @@ pub fn local_image_label_text(label_number: usize) -> String { format!("[Image #{label_number}]") } -pub fn local_image_open_tag_text(label_number: usize) -> String { +pub fn local_image_open_tag_text_with_path(label_number: usize, path: &std::path::Path) -> String { let label = local_image_label_text(label_number); - format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label}{LOCAL_IMAGE_OPEN_TAG_SUFFIX}") + let path = path.display(); + format!("{LOCAL_IMAGE_OPEN_TAG_PREFIX}{label} path=\"{path}\"{LOCAL_IMAGE_OPEN_TAG_SUFFIX}") } pub fn is_local_image_open_tag_text(text: &str) -> bool { @@ -1080,7 +1081,7 @@ pub fn local_image_content_items_with_label_number( let mut items = Vec::with_capacity(3); if let Some(label_number) = label_number { items.push(ContentItem::InputText { - text: local_image_open_tag_text(label_number), + text: local_image_open_tag_text_with_path(label_number, path), }); } items.push(ContentItem::InputImage { @@ -2866,7 +2867,7 @@ mod tests { detail: None, }, UserInput::LocalImage { - path: local_path, + path: local_path.clone(), detail: None, }, ]); @@ -2883,7 +2884,10 @@ mod tests { assert_eq!( content.get(1), Some(&ContentItem::InputText { - text: local_image_open_tag_text(/*label_number*/ 2), + text: local_image_open_tag_text_with_path( + /*label_number*/ 2, + &local_path + ), }) ); assert!(matches!( @@ -2903,6 +2907,17 @@ mod tests { Ok(()) } + #[test] + fn local_image_open_tag_preserves_path() { + assert_eq!( + local_image_open_tag_text_with_path( + /*label_number*/ 1, + std::path::Path::new(r#"/tmp/a&".png"#), + ), + r#".png">"# + ); + } + #[test] fn local_image_user_input_preserves_requested_detail() -> Result<()> { let dir = tempdir()?; diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2674492e8847..91ef624fa7ab 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -2190,8 +2190,9 @@ pub struct UserMessageEvent { #[serde(default, skip_serializing_if = "Vec::is_empty")] pub image_details: Vec>, /// Local file paths sourced from `UserInput::LocalImage`. These are kept so - /// the UI can reattach images when editing history, and should not be sent - /// to the model or treated as API-ready URLs. + /// the UI can reattach images when editing history. Local image prompts may + /// include a display form of the path, but these should not be treated as + /// API-ready URLs. #[serde(default)] pub local_images: Vec, /// Detail hints for `local_images`, indexed in parallel. Missing entries