Added images to UserMessageEvent (#3400)

This PR adds an `images` field to the existing `UserMessageEvent` so we
can encode zero or more images associated with a user message. This
allows images to be restored when conversations are restored.
This commit is contained in:
Eric Traut
2025-09-10 10:18:43 -07:00
committed by GitHub
parent 45bd5ca4b9
commit 39db113cc9
3 changed files with 95 additions and 23 deletions

View File

@@ -25,31 +25,56 @@ pub(crate) fn map_response_item_to_event_messages(
return Vec::new(); return Vec::new();
} }
let events: Vec<EventMsg> = content let mut events: Vec<EventMsg> = Vec::new();
.iter() let mut message_parts: Vec<String> = Vec::new();
.filter_map(|content_item| match content_item { let mut images: Vec<String> = Vec::new();
ContentItem::OutputText { text } => { let mut kind: Option<InputMessageKind> = None;
Some(EventMsg::AgentMessage(AgentMessageEvent {
message: text.clone(), for content_item in content.iter() {
})) match content_item {
}
ContentItem::InputText { text } => { ContentItem::InputText { text } => {
let trimmed = text.trim_start(); if kind.is_none() {
let kind = if trimmed.starts_with("<environment_context>") { let trimmed = text.trim_start();
Some(InputMessageKind::EnvironmentContext) kind = if trimmed.starts_with("<environment_context>") {
} else if trimmed.starts_with("<user_instructions>") { Some(InputMessageKind::EnvironmentContext)
Some(InputMessageKind::UserInstructions) } else if trimmed.starts_with("<user_instructions>") {
} else { Some(InputMessageKind::UserInstructions)
Some(InputMessageKind::Plain) } else {
}; Some(InputMessageKind::Plain)
Some(EventMsg::UserMessage(UserMessageEvent { };
message: text.clone(), }
kind, message_parts.push(text.clone());
}))
} }
_ => None, ContentItem::InputImage { image_url } => {
}) images.push(image_url.clone());
.collect(); }
ContentItem::OutputText { text } => {
events.push(EventMsg::AgentMessage(AgentMessageEvent {
message: text.clone(),
}));
}
}
}
if !message_parts.is_empty() || !images.is_empty() {
let message = if message_parts.is_empty() {
String::new()
} else {
message_parts.join("")
};
let images = if images.is_empty() {
None
} else {
Some(images)
};
events.push(EventMsg::UserMessage(UserMessageEvent {
message,
kind,
images,
}));
}
events events
} }
@@ -96,3 +121,47 @@ pub(crate) fn map_response_item_to_event_messages(
| ResponseItem::Other => Vec::new(), | ResponseItem::Other => Vec::new(),
} }
} }
#[cfg(test)]
mod tests {
use super::map_response_item_to_event_messages;
use crate::protocol::EventMsg;
use crate::protocol::InputMessageKind;
use codex_protocol::models::ContentItem;
use codex_protocol::models::ResponseItem;
use pretty_assertions::assert_eq;
#[test]
fn maps_user_message_with_text_and_two_images() {
let img1 = "https://example.com/one.png".to_string();
let img2 = "https://example.com/two.jpg".to_string();
let item = ResponseItem::Message {
id: None,
role: "user".to_string(),
content: vec![
ContentItem::InputText {
text: "Hello world".to_string(),
},
ContentItem::InputImage {
image_url: img1.clone(),
},
ContentItem::InputImage {
image_url: img2.clone(),
},
],
};
let events = map_response_item_to_event_messages(&item, false);
assert_eq!(events.len(), 1, "expected a single user message event");
match &events[0] {
EventMsg::UserMessage(user) => {
assert_eq!(user.message, "Hello world");
assert!(matches!(user.kind, Some(InputMessageKind::Plain)));
assert_eq!(user.images, Some(vec![img1.clone(), img2.clone()]));
}
other => panic!("expected UserMessage, got {other:?}"),
}
}
}

View File

@@ -695,6 +695,8 @@ pub struct UserMessageEvent {
pub message: String, pub message: String,
#[serde(skip_serializing_if = "Option::is_none")] #[serde(skip_serializing_if = "Option::is_none")]
pub kind: Option<InputMessageKind>, pub kind: Option<InputMessageKind>,
#[serde(skip_serializing_if = "Option::is_none")]
pub images: Option<Vec<String>>,
} }
impl<T, U> From<(T, U)> for InputMessageKind impl<T, U> From<(T, U)> for InputMessageKind

View File

@@ -144,6 +144,7 @@ fn resumed_initial_messages_render_history() {
EventMsg::UserMessage(UserMessageEvent { EventMsg::UserMessage(UserMessageEvent {
message: "hello from user".to_string(), message: "hello from user".to_string(),
kind: Some(InputMessageKind::Plain), kind: Some(InputMessageKind::Plain),
images: None,
}), }),
EventMsg::AgentMessage(AgentMessageEvent { EventMsg::AgentMessage(AgentMessageEvent {
message: "assistant reply".to_string(), message: "assistant reply".to_string(),