diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 6b129cf26a..c4b06770d3 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -86,7 +86,6 @@ use codex_protocol::config_types::Settings; use codex_protocol::config_types::WindowsSandboxLevel; use codex_protocol::items::AgentMessageContent; use codex_protocol::items::AgentMessageItem; -use codex_protocol::items::TurnItem; use codex_protocol::models::MessagePhase; use codex_protocol::models::local_image_label_text; use codex_protocol::parse_command::ParsedCommand; diff --git a/codex-rs/tui/src/chatwidget/realtime.rs b/codex-rs/tui/src/chatwidget/realtime.rs index 99aefdf4b8..c1614d04b0 100644 --- a/codex-rs/tui/src/chatwidget/realtime.rs +++ b/codex-rs/tui/src/chatwidget/realtime.rs @@ -1,6 +1,4 @@ use super::*; -use codex_core::parse_turn_item; -use codex_protocol::models::ResponseInputItem; use codex_protocol::protocol::ConversationStartParams; use codex_protocol::protocol::RealtimeAudioFrame; use codex_protocol::protocol::RealtimeConversationClosedEvent; @@ -92,14 +90,40 @@ impl ChatWidget { pub(super) fn rendered_user_message_event_from_inputs( items: &[UserInput], ) -> RenderedUserMessageEvent { - let response_item = ResponseInputItem::from(items.to_vec()).into(); - let Some(TurnItem::UserMessage(user_message)) = parse_turn_item(&response_item) else { - unreachable!("user inputs must round-trip into a user-message turn item"); - }; - let EventMsg::UserMessage(event) = user_message.as_legacy_event() else { - unreachable!("UserMessageItem::as_legacy_event must return UserMessage"); - }; - Self::rendered_user_message_event_from_event(&event) + let mut message = String::new(); + let mut remote_image_urls = Vec::new(); + let mut local_images = Vec::new(); + let mut text_elements = Vec::new(); + + for item in items { + match item { + UserInput::Text { + text, + text_elements: current_text_elements, + } => { + let offset = message.len(); + for element in current_text_elements { + text_elements.push(TextElement::new( + (offset + element.byte_range.start..offset + element.byte_range.end) + .into(), + element.placeholder(text).map(str::to_string), + )); + } + message.push_str(text); + } + UserInput::Image { image_url } => remote_image_urls.push(image_url.clone()), + UserInput::LocalImage { path } => local_images.push(path.clone()), + UserInput::Skill { .. } | UserInput::Mention { .. } => {} + _ => {} + } + } + + Self::rendered_user_message_event_from_parts( + message, + text_elements, + local_images, + remote_image_urls, + ) } pub(super) fn should_render_realtime_user_message_event( diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 4d30458679..e052823c7a 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -3612,6 +3612,48 @@ async fn live_legacy_agent_message_after_item_completed_does_not_duplicate_assis assert!(drain_insert_history(&mut rx).is_empty()); } +#[test] +fn rendered_user_message_event_from_inputs_matches_flattened_user_message_shape() { + let local_image = PathBuf::from("/tmp/local.png"); + let rendered = ChatWidget::rendered_user_message_event_from_inputs(&[ + UserInput::Text { + text: "hello ".to_string(), + text_elements: vec![TextElement::new((0..5).into(), None)], + }, + UserInput::Image { + image_url: "https://example.com/remote.png".to_string(), + }, + UserInput::LocalImage { + path: local_image.clone(), + }, + UserInput::Skill { + name: "demo".to_string(), + path: PathBuf::from("/tmp/skill/SKILL.md"), + }, + UserInput::Mention { + name: "repo".to_string(), + path: "app://repo".to_string(), + }, + UserInput::Text { + text: "world".to_string(), + text_elements: vec![TextElement::new((0..5).into(), Some("planet".to_string()))], + }, + ]); + + assert_eq!( + rendered, + ChatWidget::rendered_user_message_event_from_parts( + "hello world".to_string(), + vec![ + TextElement::new((0..5).into(), Some("hello".to_string())), + TextElement::new((6..11).into(), Some("planet".to_string())), + ], + vec![local_image], + vec!["https://example.com/remote.png".to_string()], + ) + ); +} + #[tokio::test] async fn item_completed_only_pops_front_pending_steer() { let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;