Compare commits

...

2 Commits

Author SHA1 Message Date
won
975b2e04b1 Tighten local image path prompt handling 2026-05-31 20:04:42 -07:00
won
a7298da06c Expose local image paths to models 2026-05-31 19:26:00 -07:00
4 changed files with 28 additions and 9 deletions

View File

@@ -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#"<image name=[Image #1] path="/tmp/local.png">"#.to_string();
let user_text = "Please review this image.".to_string();
let item = ResponseItem::Message {

View File

@@ -161,7 +161,7 @@ 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: format!(r#"<image name=[Image #1] path="{}">"#, abs_path.display()),
},
ContentItem::InputImage {
image_url,

View File

@@ -1026,9 +1026,16 @@ 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 {
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()
.to_string()
.replace('&', "&amp;")
.replace('"', "&quot;")
.replace('<', "&lt;")
.replace('>', "&gt;");
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 {
@@ -1087,7 +1094,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 {
@@ -2873,7 +2880,7 @@ mod tests {
detail: None,
},
UserInput::LocalImage {
path: local_path,
path: local_path.clone(),
detail: None,
},
]);
@@ -2890,7 +2897,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!(
@@ -2910,6 +2920,14 @@ mod tests {
Ok(())
}
#[test]
fn local_image_open_tag_escapes_path() {
assert_eq!(
local_image_open_tag_text_with_path(1, std::path::Path::new(r#"/tmp/a&"<b>.png"#)),
r#"<image name=[Image #1] path="/tmp/a&amp;&quot;&lt;b&gt;.png">"#
);
}
#[test]
fn local_image_user_input_preserves_requested_detail() -> Result<()> {
let dir = tempdir()?;

View File

@@ -2180,8 +2180,9 @@ pub struct UserMessageEvent {
#[serde(default, skip_serializing_if = "Vec::is_empty")]
pub image_details: Vec<Option<ImageDetail>>,
/// 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<std::path::PathBuf>,
/// Detail hints for `local_images`, indexed in parallel. Missing entries