diff --git a/codex-rs/app-server-protocol/src/protocol/thread_history.rs b/codex-rs/app-server-protocol/src/protocol/thread_history.rs index 6fa6dfabb..6772d6f92 100644 --- a/codex-rs/app-server-protocol/src/protocol/thread_history.rs +++ b/codex-rs/app-server-protocol/src/protocol/thread_history.rs @@ -197,6 +197,8 @@ impl ThreadHistoryBuilder { if !payload.message.trim().is_empty() { content.push(UserInput::Text { text: payload.message.clone(), + // TODO: Thread text element ranges into thread history. Empty keeps old behavior. + text_elements: Vec::new(), }); } if let Some(images) = &payload.images { @@ -244,6 +246,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "First turn".into(), images: Some(vec!["https://example.com/one.png".into()]), + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "Hi there".into(), @@ -257,6 +261,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "Second turn".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "Reply two".into(), @@ -277,6 +283,7 @@ mod tests { content: vec![ UserInput::Text { text: "First turn".into(), + text_elements: Vec::new(), }, UserInput::Image { url: "https://example.com/one.png".into(), @@ -308,7 +315,8 @@ mod tests { ThreadItem::UserMessage { id: "item-4".into(), content: vec![UserInput::Text { - text: "Second turn".into() + text: "Second turn".into(), + text_elements: Vec::new(), }], } ); @@ -327,6 +335,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "Turn start".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentReasoning(AgentReasoningEvent { text: "first summary".into(), @@ -371,6 +381,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "Please do the thing".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "Working...".into(), @@ -381,6 +393,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "Let's try again".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "Second attempt complete.".into(), @@ -398,7 +412,8 @@ mod tests { ThreadItem::UserMessage { id: "item-1".into(), content: vec![UserInput::Text { - text: "Please do the thing".into() + text: "Please do the thing".into(), + text_elements: Vec::new(), }], } ); @@ -418,7 +433,8 @@ mod tests { ThreadItem::UserMessage { id: "item-3".into(), content: vec![UserInput::Text { - text: "Let's try again".into() + text: "Let's try again".into(), + text_elements: Vec::new(), }], } ); @@ -437,6 +453,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "First".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "A1".into(), @@ -444,6 +462,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "Second".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "A2".into(), @@ -452,6 +472,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "Third".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "A3".into(), @@ -469,6 +491,7 @@ mod tests { id: "item-1".into(), content: vec![UserInput::Text { text: "First".into(), + text_elements: Vec::new(), }], }, ThreadItem::AgentMessage { @@ -486,6 +509,7 @@ mod tests { id: "item-3".into(), content: vec![UserInput::Text { text: "Third".into(), + text_elements: Vec::new(), }], }, ThreadItem::AgentMessage { @@ -504,6 +528,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "One".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "A1".into(), @@ -511,6 +537,8 @@ mod tests { EventMsg::UserMessage(UserMessageEvent { message: "Two".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "A2".into(), diff --git a/codex-rs/app-server-protocol/src/protocol/v2.rs b/codex-rs/app-server-protocol/src/protocol/v2.rs index 17a367e66..d8a0e8a6e 100644 --- a/codex-rs/app-server-protocol/src/protocol/v2.rs +++ b/codex-rs/app-server-protocol/src/protocol/v2.rs @@ -1543,21 +1543,55 @@ pub struct TurnInterruptParams { pub struct TurnInterruptResponse {} // User input types +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, Eq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct ByteRange { + pub start: usize, + pub end: usize, +} + +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] +#[serde(rename_all = "camelCase")] +#[ts(export_to = "v2/")] +pub struct TextElement { + /// Byte range in the parent `text` buffer that this element occupies. + pub byte_range: ByteRange, + /// Optional human-readable placeholder for the element, displayed in the UI. + pub placeholder: Option, +} + #[derive(Serialize, Deserialize, Debug, Clone, PartialEq, JsonSchema, TS)] #[serde(tag = "type", rename_all = "camelCase")] #[ts(tag = "type")] #[ts(export_to = "v2/")] pub enum UserInput { - Text { text: String }, - Image { url: String }, - LocalImage { path: PathBuf }, - Skill { name: String, path: PathBuf }, + Text { + text: String, + /// UI-defined spans within `text` used to render or persist special elements. + #[serde(default)] + text_elements: Vec, + }, + Image { + url: String, + }, + LocalImage { + path: PathBuf, + }, + Skill { + name: String, + path: PathBuf, + }, } impl UserInput { pub fn into_core(self) -> CoreUserInput { match self { - UserInput::Text { text } => CoreUserInput::Text { text }, + UserInput::Text { text, .. } => CoreUserInput::Text { + text, + // TODO: Thread text element ranges into v2 inputs. Empty keeps old behavior. + text_elements: Vec::new(), + }, UserInput::Image { url } => CoreUserInput::Image { image_url: url }, UserInput::LocalImage { path } => CoreUserInput::LocalImage { path }, UserInput::Skill { name, path } => CoreUserInput::Skill { name, path }, @@ -1568,7 +1602,11 @@ impl UserInput { impl From for UserInput { fn from(value: CoreUserInput) -> Self { match value { - CoreUserInput::Text { text } => UserInput::Text { text }, + CoreUserInput::Text { text, .. } => UserInput::Text { + text, + // TODO: Thread text element ranges from core into v2 inputs. + text_elements: Vec::new(), + }, CoreUserInput::Image { image_url } => UserInput::Image { url: image_url }, CoreUserInput::LocalImage { path } => UserInput::LocalImage { path }, CoreUserInput::Skill { name, path } => UserInput::Skill { name, path }, @@ -2160,6 +2198,7 @@ mod tests { content: vec![ CoreUserInput::Text { text: "hello".to_string(), + text_elements: Vec::new(), }, CoreUserInput::Image { image_url: "https://example.com/image.png".to_string(), @@ -2181,6 +2220,7 @@ mod tests { content: vec![ UserInput::Text { text: "hello".to_string(), + text_elements: Vec::new(), }, UserInput::Image { url: "https://example.com/image.png".to_string(), diff --git a/codex-rs/app-server-test-client/src/main.rs b/codex-rs/app-server-test-client/src/main.rs index 283a01b2c..809c2b5a3 100644 --- a/codex-rs/app-server-test-client/src/main.rs +++ b/codex-rs/app-server-test-client/src/main.rs @@ -256,7 +256,11 @@ fn send_message_v2_with_policies( println!("< thread/start response: {thread_response:?}"); let mut turn_params = TurnStartParams { thread_id: thread_response.thread.id.clone(), - input: vec![V2UserInput::Text { text: user_message }], + input: vec![V2UserInput::Text { + text: user_message, + // Plain text conversion has no UI element ranges. + text_elements: Vec::new(), + }], ..Default::default() }; turn_params.approval_policy = approval_policy; @@ -288,6 +292,7 @@ fn send_follow_up_v2( thread_id: thread_response.thread.id.clone(), input: vec![V2UserInput::Text { text: first_message, + text_elements: Vec::new(), }], ..Default::default() }; @@ -299,6 +304,7 @@ fn send_follow_up_v2( thread_id: thread_response.thread.id.clone(), input: vec![V2UserInput::Text { text: follow_up_message, + text_elements: Vec::new(), }], ..Default::default() }; diff --git a/codex-rs/app-server/src/codex_message_processor.rs b/codex-rs/app-server/src/codex_message_processor.rs index d3be7c99b..0a64ed719 100644 --- a/codex-rs/app-server/src/codex_message_processor.rs +++ b/codex-rs/app-server/src/codex_message_processor.rs @@ -3125,7 +3125,11 @@ impl CodexMessageProcessor { let mapped_items: Vec = items .into_iter() .map(|item| match item { - WireInputItem::Text { text } => CoreInputItem::Text { text }, + WireInputItem::Text { text } => CoreInputItem::Text { + text, + // TODO: Thread text element ranges into v1 input handling. + text_elements: Vec::new(), + }, WireInputItem::Image { image_url } => CoreInputItem::Image { image_url }, WireInputItem::LocalImage { path } => CoreInputItem::LocalImage { path }, }) @@ -3171,7 +3175,11 @@ impl CodexMessageProcessor { let mapped_items: Vec = items .into_iter() .map(|item| match item { - WireInputItem::Text { text } => CoreInputItem::Text { text }, + WireInputItem::Text { text } => CoreInputItem::Text { + text, + // TODO: Thread text element ranges into v1 input handling. + text_elements: Vec::new(), + }, WireInputItem::Image { image_url } => CoreInputItem::Image { image_url }, WireInputItem::LocalImage { path } => CoreInputItem::LocalImage { path }, }) @@ -3333,6 +3341,7 @@ impl CodexMessageProcessor { id: turn_id.clone(), content: vec![V2UserInput::Text { text: display_text.to_string(), + text_elements: Vec::new(), }], }] }; diff --git a/codex-rs/app-server/tests/suite/v2/output_schema.rs b/codex-rs/app-server/tests/suite/v2/output_schema.rs index f23c03703..149e098b6 100644 --- a/codex-rs/app-server/tests/suite/v2/output_schema.rs +++ b/codex-rs/app-server/tests/suite/v2/output_schema.rs @@ -61,6 +61,7 @@ async fn turn_start_accepts_output_schema_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "Hello".to_string(), + text_elements: Vec::new(), }], output_schema: Some(output_schema.clone()), ..Default::default() @@ -142,6 +143,7 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "Hello".to_string(), + text_elements: Vec::new(), }], output_schema: Some(output_schema.clone()), ..Default::default() @@ -183,6 +185,7 @@ async fn turn_start_output_schema_is_per_turn_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "Hello again".to_string(), + text_elements: Vec::new(), }], output_schema: None, ..Default::default() diff --git a/codex-rs/app-server/tests/suite/v2/thread_fork.rs b/codex-rs/app-server/tests/suite/v2/thread_fork.rs index a5445998f..32abc237e 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_fork.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_fork.rs @@ -95,7 +95,8 @@ async fn thread_fork_creates_new_thread_and_emits_started() -> Result<()> { assert_eq!( content, &vec![UserInput::Text { - text: preview.to_string() + text: preview.to_string(), + text_elements: Vec::new(), }] ); } diff --git a/codex-rs/app-server/tests/suite/v2/thread_resume.rs b/codex-rs/app-server/tests/suite/v2/thread_resume.rs index 483095a98..cfcfcedf7 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_resume.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_resume.rs @@ -118,7 +118,8 @@ async fn thread_resume_returns_rollout_history() -> Result<()> { assert_eq!( content, &vec![UserInput::Text { - text: preview.to_string() + text: preview.to_string(), + text_elements: Vec::new(), }] ); } diff --git a/codex-rs/app-server/tests/suite/v2/thread_rollback.rs b/codex-rs/app-server/tests/suite/v2/thread_rollback.rs index e88c065f7..6e3767db9 100644 --- a/codex-rs/app-server/tests/suite/v2/thread_rollback.rs +++ b/codex-rs/app-server/tests/suite/v2/thread_rollback.rs @@ -57,6 +57,7 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<() thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: first_text.to_string(), + text_elements: Vec::new(), }], ..Default::default() }) @@ -77,6 +78,7 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<() thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "Second".to_string(), + text_elements: Vec::new(), }], ..Default::default() }) @@ -115,7 +117,8 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<() assert_eq!( content, &vec![V2UserInput::Text { - text: first_text.to_string() + text: first_text.to_string(), + text_elements: Vec::new(), }] ); } @@ -143,7 +146,8 @@ async fn thread_rollback_drops_last_turns_and_persists_to_rollout() -> Result<() assert_eq!( content, &vec![V2UserInput::Text { - text: first_text.to_string() + text: first_text.to_string(), + text_elements: Vec::new(), }] ); } diff --git a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs index 34587793c..9c804aa66 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_interrupt.rs @@ -73,6 +73,7 @@ async fn turn_interrupt_aborts_running_turn() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "run sleep".to_string(), + text_elements: Vec::new(), }], cwd: Some(working_directory.clone()), ..Default::default() diff --git a/codex-rs/app-server/tests/suite/v2/turn_start.rs b/codex-rs/app-server/tests/suite/v2/turn_start.rs index d798e7d34..406f80328 100644 --- a/codex-rs/app-server/tests/suite/v2/turn_start.rs +++ b/codex-rs/app-server/tests/suite/v2/turn_start.rs @@ -80,6 +80,7 @@ async fn turn_start_sends_originator_header() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "Hello".to_string(), + text_elements: Vec::new(), }], ..Default::default() }) @@ -149,6 +150,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "Hello".to_string(), + text_elements: Vec::new(), }], ..Default::default() }) @@ -181,6 +183,7 @@ async fn turn_start_emits_notifications_and_accepts_model_override() -> Result<( thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "Second".to_string(), + text_elements: Vec::new(), }], model: Some("mock-model-override".to_string()), ..Default::default() @@ -331,6 +334,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "run python".to_string(), + text_elements: Vec::new(), }], ..Default::default() }) @@ -376,6 +380,7 @@ async fn turn_start_exec_approval_toggle_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "run python again".to_string(), + text_elements: Vec::new(), }], approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), sandbox_policy: Some(codex_app_server_protocol::SandboxPolicy::DangerFullAccess), @@ -452,6 +457,7 @@ async fn turn_start_exec_approval_decline_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "run python".to_string(), + text_elements: Vec::new(), }], cwd: Some(workspace.clone()), ..Default::default() @@ -600,6 +606,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "first turn".to_string(), + text_elements: Vec::new(), }], cwd: Some(first_cwd.clone()), approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), @@ -633,6 +640,7 @@ async fn turn_start_updates_sandbox_and_cwd_between_turns_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "second turn".to_string(), + text_elements: Vec::new(), }], cwd: Some(second_cwd.clone()), approval_policy: Some(codex_app_server_protocol::AskForApproval::Never), @@ -733,6 +741,7 @@ async fn turn_start_file_change_approval_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "apply patch".into(), + text_elements: Vec::new(), }], cwd: Some(workspace.clone()), ..Default::default() @@ -910,6 +919,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "apply patch 1".into(), + text_elements: Vec::new(), }], cwd: Some(workspace.clone()), ..Default::default() @@ -986,6 +996,7 @@ async fn turn_start_file_change_approval_accept_for_session_persists_v2() -> Res thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "apply patch 2".into(), + text_elements: Vec::new(), }], cwd: Some(workspace.clone()), ..Default::default() @@ -1083,6 +1094,7 @@ async fn turn_start_file_change_approval_decline_v2() -> Result<()> { thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "apply patch".into(), + text_elements: Vec::new(), }], cwd: Some(workspace.clone()), ..Default::default() @@ -1230,6 +1242,7 @@ unified_exec = true thread_id: thread.id.clone(), input: vec![V2UserInput::Text { text: "run a command".to_string(), + text_elements: Vec::new(), }], ..Default::default() }) diff --git a/codex-rs/core/src/agent/control.rs b/codex-rs/core/src/agent/control.rs index f547a6d4d..c8ba7fdda 100644 --- a/codex-rs/core/src/agent/control.rs +++ b/codex-rs/core/src/agent/control.rs @@ -56,7 +56,11 @@ impl AgentControl { .send_op( agent_id, Op::UserInput { - items: vec![UserInput::Text { text: prompt }], + items: vec![UserInput::Text { + text: prompt, + // Plain text conversion has no UI element ranges. + text_elements: Vec::new(), + }], final_output_json_schema: None, }, ) @@ -321,6 +325,7 @@ mod tests { Op::UserInput { items: vec![UserInput::Text { text: "hello from tests".to_string(), + text_elements: Vec::new(), }], final_output_json_schema: None, }, @@ -351,6 +356,7 @@ mod tests { Op::UserInput { items: vec![UserInput::Text { text: "spawned".to_string(), + text_elements: Vec::new(), }], final_output_json_schema: None, }, diff --git a/codex-rs/core/src/codex.rs b/codex-rs/core/src/codex.rs index da0719b00..158443ee4 100644 --- a/codex-rs/core/src/codex.rs +++ b/codex-rs/core/src/codex.rs @@ -2260,6 +2260,7 @@ mod handlers { Arc::clone(&turn_context), vec![UserInput::Text { text: turn_context.compact_prompt().to_string(), + text_elements: Vec::new(), }], CompactTask, ) @@ -2470,6 +2471,7 @@ async fn spawn_review_thread( // Seed the child task with the review prompt as the initial user message. let input: Vec = vec![UserInput::Text { text: review_prompt, + text_elements: Vec::new(), }]; let tc = Arc::new(review_turn_context); sess.spawn_task(tc.clone(), input, ReviewTask::new()).await; @@ -3964,6 +3966,7 @@ mod tests { let (sess, tc, rx) = make_session_and_context_with_rx().await; let input = vec![UserInput::Text { text: "hello".to_string(), + text_elements: Vec::new(), }]; sess.spawn_task( Arc::clone(&tc), @@ -3993,6 +3996,7 @@ mod tests { let (sess, tc, rx) = make_session_and_context_with_rx().await; let input = vec![UserInput::Text { text: "hello".to_string(), + text_elements: Vec::new(), }]; sess.spawn_task( Arc::clone(&tc), @@ -4019,6 +4023,7 @@ mod tests { let (sess, tc, rx) = make_session_and_context_with_rx().await; let input = vec![UserInput::Text { text: "start review".to_string(), + text_elements: Vec::new(), }]; sess.spawn_task(Arc::clone(&tc), input, ReviewTask::new()) .await; diff --git a/codex-rs/core/src/compact.rs b/codex-rs/core/src/compact.rs index 120e701bb..4dc56f10d 100644 --- a/codex-rs/core/src/compact.rs +++ b/codex-rs/core/src/compact.rs @@ -44,7 +44,11 @@ pub(crate) async fn run_inline_auto_compact_task( turn_context: Arc, ) { let prompt = turn_context.compact_prompt().to_string(); - let input = vec![UserInput::Text { text: prompt }]; + let input = vec![UserInput::Text { + text: prompt, + // Plain text conversion has no UI element ranges. + text_elements: Vec::new(), + }]; run_compact_task_inner(sess, turn_context, input).await; } diff --git a/codex-rs/core/src/event_mapping.rs b/codex-rs/core/src/event_mapping.rs index fe592236c..6c19c726d 100644 --- a/codex-rs/core/src/event_mapping.rs +++ b/codex-rs/core/src/event_mapping.rs @@ -50,7 +50,11 @@ fn parse_user_message(message: &[ContentItem]) -> Option { if is_session_prefix(text) || is_user_shell_command_text(text) { return None; } - content.push(UserInput::Text { text: text.clone() }); + content.push(UserInput::Text { + text: text.clone(), + // Plain text conversion has no UI element ranges. + text_elements: Vec::new(), + }); } ContentItem::InputImage { image_url } => { content.push(UserInput::Image { @@ -179,6 +183,7 @@ mod tests { let expected_content = vec![ UserInput::Text { text: "Hello world".to_string(), + text_elements: Vec::new(), }, UserInput::Image { image_url: img1 }, UserInput::Image { image_url: img2 }, @@ -218,7 +223,10 @@ mod tests { TurnItem::UserMessage(user) => { let expected_content = vec![ UserInput::Image { image_url }, - UserInput::Text { text: user_text }, + UserInput::Text { + text: user_text, + text_elements: Vec::new(), + }, ]; assert_eq!(user.content, expected_content); } @@ -255,7 +263,10 @@ mod tests { TurnItem::UserMessage(user) => { let expected_content = vec![ UserInput::Image { image_url }, - UserInput::Text { text: user_text }, + UserInput::Text { + text: user_text, + text_elements: Vec::new(), + }, ]; assert_eq!(user.content, expected_content); } diff --git a/codex-rs/core/src/rollout/tests.rs b/codex-rs/core/src/rollout/tests.rs index f7c13c70f..c16ec65cf 100644 --- a/codex-rs/core/src/rollout/tests.rs +++ b/codex-rs/core/src/rollout/tests.rs @@ -603,6 +603,8 @@ async fn test_updated_at_uses_file_mtime() -> Result<()> { item: RolloutItem::EventMsg(EventMsg::UserMessage(UserMessageEvent { message: "hello".into(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), })), }; writeln!(file, "{}", serde_json::to_string(&user_event_line)?)?; diff --git a/codex-rs/core/tests/common/test_codex.rs b/codex-rs/core/tests/common/test_codex.rs index 6d59cd4df..7bf081948 100644 --- a/codex-rs/core/tests/common/test_codex.rs +++ b/codex-rs/core/tests/common/test_codex.rs @@ -270,6 +270,7 @@ impl TestCodex { .submit(Op::UserTurn { items: vec![UserInput::Text { text: prompt.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: self.cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/abort_tasks.rs b/codex-rs/core/tests/suite/abort_tasks.rs index 094c10c77..c020fa00c 100644 --- a/codex-rs/core/tests/suite/abort_tasks.rs +++ b/codex-rs/core/tests/suite/abort_tasks.rs @@ -48,6 +48,7 @@ async fn interrupt_long_running_tool_emits_turn_aborted() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "start sleep".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -101,6 +102,7 @@ async fn interrupt_tool_records_history_entries() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "start history recording".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -118,6 +120,7 @@ async fn interrupt_tool_records_history_entries() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "follow up".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/apply_patch_cli.rs b/codex-rs/core/tests/suite/apply_patch_cli.rs index f4515a963..5f764d227 100644 --- a/codex-rs/core/tests/suite/apply_patch_cli.rs +++ b/codex-rs/core/tests/suite/apply_patch_cli.rs @@ -302,6 +302,7 @@ async fn apply_patch_cli_move_without_content_change_has_no_turn_diff( .submit(Op::UserTurn { items: vec![UserInput::Text { text: "rename without content change".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -888,6 +889,7 @@ async fn apply_patch_shell_command_heredoc_with_cd_emits_turn_diff() -> Result<( .submit(Op::UserTurn { items: vec![UserInput::Text { text: "apply via shell heredoc with cd".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -965,6 +967,7 @@ async fn apply_patch_shell_command_failure_propagates_error_and_skips_diff() -> .submit(Op::UserTurn { items: vec![UserInput::Text { text: "apply patch via shell".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1112,6 +1115,7 @@ async fn apply_patch_emits_turn_diff_event_with_unified_diff( .submit(Op::UserTurn { items: vec![UserInput::Text { text: "emit diff".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1172,6 +1176,7 @@ async fn apply_patch_turn_diff_for_rename_with_content_change( .submit(Op::UserTurn { items: vec![UserInput::Text { text: "rename with change".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1240,6 +1245,7 @@ async fn apply_patch_aggregates_diff_across_multiple_tool_calls() -> Result<()> .submit(Op::UserTurn { items: vec![UserInput::Text { text: "aggregate diffs".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1308,6 +1314,7 @@ async fn apply_patch_aggregates_diff_preserves_success_after_failure() -> Result .submit(Op::UserTurn { items: vec![UserInput::Text { text: "apply patch twice with failure".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/approvals.rs b/codex-rs/core/tests/suite/approvals.rs index 51f1c31f1..357fdd124 100644 --- a/codex-rs/core/tests/suite/approvals.rs +++ b/codex-rs/core/tests/suite/approvals.rs @@ -492,6 +492,7 @@ async fn submit_turn( .submit(Op::UserTurn { items: vec![UserInput::Text { text: prompt.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: test.cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/client.rs b/codex-rs/core/tests/suite/client.rs index ecb8dcbbf..18d3caf3b 100644 --- a/codex-rs/core/tests/suite/client.rs +++ b/codex-rs/core/tests/suite/client.rs @@ -289,6 +289,7 @@ async fn resume_includes_initial_messages_and_sends_prior_items() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -387,6 +388,7 @@ async fn includes_conversation_id_and_model_headers_in_request() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -440,6 +442,7 @@ async fn includes_base_instructions_override_in_request() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -496,6 +499,7 @@ async fn chatgpt_auth_sends_correct_request() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -588,6 +592,7 @@ async fn prefers_apikey_when_config_prefers_apikey_even_with_chatgpt_tokens() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -629,6 +634,7 @@ async fn includes_user_instructions_message_in_request() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -708,6 +714,7 @@ async fn skills_append_to_instructions() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -760,6 +767,7 @@ async fn includes_configured_effort_in_request() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -797,6 +805,7 @@ async fn includes_no_effort_in_request() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -832,6 +841,7 @@ async fn includes_default_reasoning_effort_in_request_when_defined_by_model_info .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -871,6 +881,7 @@ async fn configured_reasoning_summary_is_sent() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -910,6 +921,7 @@ async fn reasoning_summary_is_omitted_when_disabled() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -943,6 +955,7 @@ async fn includes_default_verbosity_in_request() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -983,6 +996,7 @@ async fn configured_verbosity_not_sent_for_models_without_support() -> anyhow::R .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1022,6 +1036,7 @@ async fn configured_verbosity_is_sent() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1077,6 +1092,7 @@ async fn includes_developer_instructions_message_in_request() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1326,6 +1342,7 @@ async fn token_count_includes_rate_limits_snapshot() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1484,6 +1501,7 @@ async fn usage_limit_error_emits_rate_limit_event() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1554,6 +1572,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res .submit(Op::UserInput { items: vec![UserInput::Text { text: "seed turn".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1565,6 +1584,7 @@ async fn context_window_error_sets_total_tokens_to_model_window() -> anyhow::Res .submit(Op::UserInput { items: vec![UserInput::Text { text: "trigger context window".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1685,6 +1705,7 @@ async fn azure_overrides_assign_properties_used_for_responses_url() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1768,6 +1789,7 @@ async fn env_var_overrides_loaded_auth() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1840,7 +1862,10 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { // Turn 1: user sends U1; wait for completion. codex .submit(Op::UserInput { - items: vec![UserInput::Text { text: "U1".into() }], + items: vec![UserInput::Text { + text: "U1".into(), + text_elements: Vec::new(), + }], final_output_json_schema: None, }) .await @@ -1850,7 +1875,10 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { // Turn 2: user sends U2; wait for completion. codex .submit(Op::UserInput { - items: vec![UserInput::Text { text: "U2".into() }], + items: vec![UserInput::Text { + text: "U2".into(), + text_elements: Vec::new(), + }], final_output_json_schema: None, }) .await @@ -1860,7 +1888,10 @@ async fn history_dedupes_streamed_and_final_messages_across_turns() { // Turn 3: user sends U3; wait for completion. codex .submit(Op::UserInput { - items: vec![UserInput::Text { text: "U3".into() }], + items: vec![UserInput::Text { + text: "U3".into(), + text_elements: Vec::new(), + }], final_output_json_schema: None, }) .await diff --git a/codex-rs/core/tests/suite/compact.rs b/codex-rs/core/tests/suite/compact.rs index e3c8e0b7c..79bac55a8 100644 --- a/codex-rs/core/tests/suite/compact.rs +++ b/codex-rs/core/tests/suite/compact.rs @@ -159,6 +159,7 @@ async fn summarize_context_three_requests_and_instructions() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello world".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -180,6 +181,7 @@ async fn summarize_context_three_requests_and_instructions() { .submit(Op::UserInput { items: vec![UserInput::Text { text: THIRD_USER_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -573,6 +575,7 @@ async fn multiple_auto_compact_per_task_runs_after_token_limit_hit() { .submit(Op::UserInput { items: vec![UserInput::Text { text: user_message.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1049,6 +1052,7 @@ async fn auto_compact_runs_after_token_limit_hit() { .submit(Op::UserInput { items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1061,6 +1065,7 @@ async fn auto_compact_runs_after_token_limit_hit() { .submit(Op::UserInput { items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1073,6 +1078,7 @@ async fn auto_compact_runs_after_token_limit_hit() { .submit(Op::UserInput { items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1273,6 +1279,7 @@ async fn auto_compact_runs_after_resume_when_token_usage_is_over_limit() { .submit(Op::UserTurn { items: vec![UserInput::Text { text: follow_up_user.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: resumed.cwd.path().to_path_buf(), @@ -1382,6 +1389,7 @@ async fn auto_compact_persists_rollout_entries() { .submit(Op::UserInput { items: vec![UserInput::Text { text: FIRST_AUTO_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1393,6 +1401,7 @@ async fn auto_compact_persists_rollout_entries() { .submit(Op::UserInput { items: vec![UserInput::Text { text: SECOND_AUTO_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1404,6 +1413,7 @@ async fn auto_compact_persists_rollout_entries() { .submit(Op::UserInput { items: vec![UserInput::Text { text: POST_AUTO_USER_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1496,6 +1506,7 @@ async fn manual_compact_retries_after_context_window_error() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "first turn".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1629,6 +1640,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { .submit(Op::UserInput { items: vec![UserInput::Text { text: first_user_message.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1643,6 +1655,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { .submit(Op::UserInput { items: vec![UserInput::Text { text: second_user_message.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1657,6 +1670,7 @@ async fn manual_compact_twice_preserves_latest_user_messages() { .submit(Op::UserInput { items: vec![UserInput::Text { text: final_user_message.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1835,7 +1849,10 @@ async fn auto_compact_allows_multiple_attempts_when_interleaved_with_other_turn_ for user in [MULTI_AUTO_MSG, follow_up_user, final_user] { codex .submit(Op::UserInput { - items: vec![UserInput::Text { text: user.into() }], + items: vec![UserInput::Text { + text: user.into(), + text_elements: Vec::new(), + }], final_output_json_schema: None, }) .await @@ -1948,6 +1965,7 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() { .submit(Op::UserInput { items: vec![UserInput::Text { text: FUNCTION_CALL_LIMIT_MSG.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1960,6 +1978,7 @@ async fn auto_compact_triggers_after_function_call_over_95_percent_usage() { .submit(Op::UserInput { items: vec![UserInput::Text { text: follow_up_user.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -2075,7 +2094,10 @@ async fn auto_compact_counts_encrypted_reasoning_before_last_user() { { codex .submit(Op::UserInput { - items: vec![UserInput::Text { text: user.into() }], + items: vec![UserInput::Text { + text: user.into(), + text_elements: Vec::new(), + }], final_output_json_schema: None, }) .await diff --git a/codex-rs/core/tests/suite/compact_remote.rs b/codex-rs/core/tests/suite/compact_remote.rs index 1c598a9ab..a18baab54 100644 --- a/codex-rs/core/tests/suite/compact_remote.rs +++ b/codex-rs/core/tests/suite/compact_remote.rs @@ -73,6 +73,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello remote compact".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -86,6 +87,7 @@ async fn remote_compact_replaces_history_for_followups() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "after compact".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -192,6 +194,7 @@ async fn remote_compact_runs_automatically() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello remote compact".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -265,6 +268,7 @@ async fn remote_compact_persists_replacement_history_in_rollout() -> Result<()> .submit(Op::UserInput { items: vec![UserInput::Text { text: "needs compaction".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/compact_resume_fork.rs b/codex-rs/core/tests/suite/compact_resume_fork.rs index a8de9e5c1..e054908ae 100644 --- a/codex-rs/core/tests/suite/compact_resume_fork.rs +++ b/codex-rs/core/tests/suite/compact_resume_fork.rs @@ -983,7 +983,10 @@ async fn start_test_conversation( async fn user_turn(conversation: &Arc, text: &str) { conversation .submit(Op::UserInput { - items: vec![UserInput::Text { text: text.into() }], + items: vec![UserInput::Text { + text: text.into(), + text_elements: Vec::new(), + }], final_output_json_schema: None, }) .await diff --git a/codex-rs/core/tests/suite/exec_policy.rs b/codex-rs/core/tests/suite/exec_policy.rs index 48471e457..c7ae447bd 100644 --- a/codex-rs/core/tests/suite/exec_policy.rs +++ b/codex-rs/core/tests/suite/exec_policy.rs @@ -72,6 +72,7 @@ async fn execpolicy_blocks_shell_invocation() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run shell command".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/fork_thread.rs b/codex-rs/core/tests/suite/fork_thread.rs index e2e653575..2a9f604c3 100644 --- a/codex-rs/core/tests/suite/fork_thread.rs +++ b/codex-rs/core/tests/suite/fork_thread.rs @@ -70,6 +70,7 @@ async fn fork_thread_twice_drops_to_first_message() { .submit(Op::UserInput { items: vec![UserInput::Text { text: text.to_string(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/image_rollout.rs b/codex-rs/core/tests/suite/image_rollout.rs index 401c80912..0df7c7e73 100644 --- a/codex-rs/core/tests/suite/image_rollout.rs +++ b/codex-rs/core/tests/suite/image_rollout.rs @@ -117,6 +117,7 @@ async fn copy_paste_local_image_persists_rollout_request_shape() -> anyhow::Resu }, UserInput::Text { text: "pasted image".to_string(), + text_elements: Vec::new(), }, ], final_output_json_schema: None, @@ -194,6 +195,7 @@ async fn drag_drop_image_persists_rollout_request_shape() -> anyhow::Result<()> }, UserInput::Text { text: "dropped image".to_string(), + text_elements: Vec::new(), }, ], final_output_json_schema: None, diff --git a/codex-rs/core/tests/suite/items.rs b/codex-rs/core/tests/suite/items.rs index 3c6cf5ff3..ce9b0f134 100644 --- a/codex-rs/core/tests/suite/items.rs +++ b/codex-rs/core/tests/suite/items.rs @@ -42,6 +42,7 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { .submit(Op::UserInput { items: (vec![UserInput::Text { text: "please inspect sample.txt".into(), + text_elements: Vec::new(), }]), final_output_json_schema: None, }) @@ -69,12 +70,14 @@ async fn user_message_item_is_emitted() -> anyhow::Result<()> { started_item.content, vec![UserInput::Text { text: "please inspect sample.txt".into(), + text_elements: Vec::new(), }] ); assert_eq!( completed_item.content, vec![UserInput::Text { text: "please inspect sample.txt".into(), + text_elements: Vec::new(), }] ); Ok(()) @@ -99,6 +102,7 @@ async fn assistant_message_item_is_emitted() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "please summarize results".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -156,6 +160,7 @@ async fn reasoning_item_is_emitted() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "explain your reasoning".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -215,6 +220,7 @@ async fn web_search_item_is_emitted() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "find the weather".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -268,6 +274,7 @@ async fn agent_message_content_delta_has_item_metadata() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "please stream text".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -334,6 +341,7 @@ async fn reasoning_content_delta_has_item_metadata() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "reason through it".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -392,6 +400,7 @@ async fn reasoning_raw_content_delta_respects_flag() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "show raw reasoning".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/json_result.rs b/codex-rs/core/tests/suite/json_result.rs index 1b9949102..d90cb217c 100644 --- a/codex-rs/core/tests/suite/json_result.rs +++ b/codex-rs/core/tests/suite/json_result.rs @@ -76,6 +76,7 @@ async fn codex_returns_json_result(model: String) -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "hello world".into(), + text_elements: Vec::new(), }], final_output_json_schema: Some(serde_json::from_str(SCHEMA)?), cwd: cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/models_cache_ttl.rs b/codex-rs/core/tests/suite/models_cache_ttl.rs index af8c85315..706a3dea5 100644 --- a/codex-rs/core/tests/suite/models_cache_ttl.rs +++ b/codex-rs/core/tests/suite/models_cache_ttl.rs @@ -87,7 +87,10 @@ async fn renews_cache_ttl_on_matching_models_etag() -> Result<()> { codex .submit(Op::UserTurn { - items: vec![UserInput::Text { text: "hi".into() }], + items: vec![UserInput::Text { + text: "hi".into(), + text_elements: Vec::new(), + }], final_output_json_schema: None, cwd: test.cwd_path().to_path_buf(), approval_policy: codex_core::protocol::AskForApproval::Never, diff --git a/codex-rs/core/tests/suite/models_etag_responses.rs b/codex-rs/core/tests/suite/models_etag_responses.rs index a733800cb..4976b2881 100644 --- a/codex-rs/core/tests/suite/models_etag_responses.rs +++ b/codex-rs/core/tests/suite/models_etag_responses.rs @@ -100,6 +100,7 @@ async fn refresh_models_on_models_etag_mismatch_and_avoid_duplicate_models_fetch .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please run a tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/otel.rs b/codex-rs/core/tests/suite/otel.rs index ed7c8fb04..5138a145b 100644 --- a/codex-rs/core/tests/suite/otel.rs +++ b/codex-rs/core/tests/suite/otel.rs @@ -45,6 +45,7 @@ async fn responses_api_emits_api_request_event() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -87,6 +88,7 @@ async fn process_sse_emits_tracing_for_output_item() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -126,6 +128,7 @@ async fn process_sse_emits_failed_event_on_parse_error() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -166,6 +169,7 @@ async fn process_sse_records_failed_event_when_stream_closes_without_completed() .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -226,6 +230,7 @@ async fn process_sse_failed_event_records_response_error_message() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -284,6 +289,7 @@ async fn process_sse_failed_event_logs_parse_error() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -329,6 +335,7 @@ async fn process_sse_failed_event_logs_missing_error() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -383,6 +390,7 @@ async fn process_sse_failed_event_logs_response_completed_parse_error() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -434,6 +442,7 @@ async fn process_sse_emits_completed_telemetry() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -502,6 +511,7 @@ async fn handle_responses_span_records_response_kind_and_tool_name() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -567,6 +577,7 @@ async fn record_responses_sets_span_fields_for_response_events() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -647,6 +658,7 @@ async fn handle_response_item_records_tool_result_for_custom_tool_call() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -715,6 +727,7 @@ async fn handle_response_item_records_tool_result_for_function_call() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -793,6 +806,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_missing_ids() .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -855,6 +869,7 @@ async fn handle_response_item_records_tool_result_for_local_shell_call() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -960,6 +975,7 @@ async fn handle_container_exec_autoapprove_from_config_records_tool_decision() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1009,6 +1025,7 @@ async fn handle_container_exec_user_approved_records_tool_decision() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "approved".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1068,6 +1085,7 @@ async fn handle_container_exec_user_approved_for_session_records_tool_decision() .submit(Op::UserInput { items: vec![UserInput::Text { text: "persist".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1127,6 +1145,7 @@ async fn handle_sandbox_error_user_approves_retry_records_tool_decision() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "retry".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1186,6 +1205,7 @@ async fn handle_container_exec_user_denies_records_tool_decision() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "deny".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1245,6 +1265,7 @@ async fn handle_sandbox_error_user_approves_for_session_records_tool_decision() .submit(Op::UserInput { items: vec![UserInput::Text { text: "persist".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -1305,6 +1326,7 @@ async fn handle_sandbox_error_user_denies_records_tool_decision() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "deny".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/pending_input.rs b/codex-rs/core/tests/suite/pending_input.rs index 3fbf79fa0..ad662d901 100644 --- a/codex-rs/core/tests/suite/pending_input.rs +++ b/codex-rs/core/tests/suite/pending_input.rs @@ -100,6 +100,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "first prompt".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -115,6 +116,7 @@ async fn injected_user_input_triggers_follow_up_request_with_deltas() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "second prompt".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/permissions_messages.rs b/codex-rs/core/tests/suite/permissions_messages.rs index 3f8a9b490..6f676c1da 100644 --- a/codex-rs/core/tests/suite/permissions_messages.rs +++ b/codex-rs/core/tests/suite/permissions_messages.rs @@ -61,6 +61,7 @@ async fn permissions_message_sent_once_on_start() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -93,6 +94,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -114,6 +116,7 @@ async fn permissions_message_added_on_override_change() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -152,6 +155,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -162,6 +166,7 @@ async fn permissions_message_not_added_when_no_change() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -203,6 +208,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -226,6 +232,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -238,6 +245,7 @@ async fn resume_replays_permissions_messages() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "after resume".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -276,6 +284,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -299,6 +308,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -319,6 +329,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "after resume".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -346,6 +357,7 @@ async fn resume_and_fork_append_permissions_messages() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "after fork".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -393,6 +405,7 @@ async fn permissions_message_includes_writable_roots() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/prompt_caching.rs b/codex-rs/core/tests/suite/prompt_caching.rs index 303e958b3..b3ed78f69 100644 --- a/codex-rs/core/tests/suite/prompt_caching.rs +++ b/codex-rs/core/tests/suite/prompt_caching.rs @@ -108,6 +108,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -118,6 +119,7 @@ async fn prompt_tools_are_consistent_across_requests() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -180,6 +182,7 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -190,6 +193,7 @@ async fn codex_mini_latest_tools() -> anyhow::Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -241,6 +245,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -251,6 +256,7 @@ async fn prefixes_context_and_instructions_once_and_consistently_across_requests .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -316,6 +322,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -345,6 +352,7 @@ async fn overrides_turn_context_but_keeps_cached_prefix_and_key_constant() -> an .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -406,6 +414,7 @@ async fn override_before_first_turn_emits_environment_context() -> anyhow::Resul .submit(Op::UserInput { items: vec![UserInput::Text { text: "first message".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -517,6 +526,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -536,6 +546,7 @@ async fn per_turn_overrides_keep_cached_prefix_and_key_constant() -> anyhow::Res .submit(Op::UserTurn { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], cwd: new_cwd.path().to_path_buf(), approval_policy: AskForApproval::Never, @@ -627,6 +638,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a .submit(Op::UserTurn { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], cwd: default_cwd.clone(), approval_policy: default_approval_policy, @@ -643,6 +655,7 @@ async fn send_user_turn_with_no_changes_does_not_send_environment_context() -> a .submit(Op::UserTurn { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], cwd: default_cwd.clone(), approval_policy: default_approval_policy, @@ -720,6 +733,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu .submit(Op::UserTurn { items: vec![UserInput::Text { text: "hello 1".into(), + text_elements: Vec::new(), }], cwd: default_cwd.clone(), approval_policy: default_approval_policy, @@ -736,6 +750,7 @@ async fn send_user_turn_with_changes_sends_environment_context() -> anyhow::Resu .submit(Op::UserTurn { items: vec![UserInput::Text { text: "hello 2".into(), + text_elements: Vec::new(), }], cwd: default_cwd.clone(), approval_policy: AskForApproval::Never, diff --git a/codex-rs/core/tests/suite/quota_exceeded.rs b/codex-rs/core/tests/suite/quota_exceeded.rs index 17fc50614..afa23275e 100644 --- a/codex-rs/core/tests/suite/quota_exceeded.rs +++ b/codex-rs/core/tests/suite/quota_exceeded.rs @@ -43,6 +43,7 @@ async fn quota_exceeded_emits_single_error_event() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "quota?".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/remote_models.rs b/codex-rs/core/tests/suite/remote_models.rs index 38d010971..75c22df55 100644 --- a/codex-rs/core/tests/suite/remote_models.rs +++ b/codex-rs/core/tests/suite/remote_models.rs @@ -165,6 +165,7 @@ async fn remote_models_remote_model_uses_unified_exec() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run call".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -369,6 +370,7 @@ async fn remote_models_apply_remote_base_instructions() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "hello remote".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/request_compression.rs b/codex-rs/core/tests/suite/request_compression.rs index 50e3fd921..9a99b913f 100644 --- a/codex-rs/core/tests/suite/request_compression.rs +++ b/codex-rs/core/tests/suite/request_compression.rs @@ -39,6 +39,7 @@ async fn request_body_is_zstd_compressed_for_codex_backend_when_enabled() -> any .submit(Op::UserInput { items: vec![UserInput::Text { text: "compress me".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -82,6 +83,7 @@ async fn request_body_is_not_compressed_for_api_key_auth_even_when_enabled() -> .submit(Op::UserInput { items: vec![UserInput::Text { text: "do not compress".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/resume.rs b/codex-rs/core/tests/suite/resume.rs index 442075a6f..8424146d6 100644 --- a/codex-rs/core/tests/suite/resume.rs +++ b/codex-rs/core/tests/suite/resume.rs @@ -36,6 +36,7 @@ async fn resume_includes_initial_messages_from_rollout_events() -> Result<()> { .submit(Op::UserInput { items: vec![UserInput::Text { text: "Record some messages".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -89,6 +90,7 @@ async fn resume_includes_initial_messages_from_reasoning_events() -> Result<()> .submit(Op::UserInput { items: vec![UserInput::Text { text: "Record reasoning messages".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/review.rs b/codex-rs/core/tests/suite/review.rs index e0870b376..d182af825 100644 --- a/codex-rs/core/tests/suite/review.rs +++ b/codex-rs/core/tests/suite/review.rs @@ -707,6 +707,7 @@ async fn review_history_surfaces_in_parent_session() { .submit(Op::UserInput { items: vec![UserInput::Text { text: followup.clone(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/rmcp_client.rs b/codex-rs/core/tests/suite/rmcp_client.rs index eef1af00f..5001eead3 100644 --- a/codex-rs/core/tests/suite/rmcp_client.rs +++ b/codex-rs/core/tests/suite/rmcp_client.rs @@ -108,6 +108,7 @@ async fn stdio_server_round_trip() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "call the rmcp echo tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), @@ -244,6 +245,7 @@ async fn stdio_image_responses_round_trip() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "call the rmcp image tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), @@ -438,6 +440,7 @@ async fn stdio_image_completions_round_trip() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "call the rmcp image tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), @@ -580,6 +583,7 @@ async fn stdio_server_propagates_whitelisted_env_vars() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "call the rmcp echo tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), @@ -733,6 +737,7 @@ async fn streamable_http_tool_call_round_trip() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "call the rmcp streamable http echo tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), @@ -918,6 +923,7 @@ async fn streamable_http_with_oauth_round_trip() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "call the rmcp streamable http oauth echo tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/shell_snapshot.rs b/codex-rs/core/tests/suite/shell_snapshot.rs index 39f2b3a33..74ff56e84 100644 --- a/codex-rs/core/tests/suite/shell_snapshot.rs +++ b/codex-rs/core/tests/suite/shell_snapshot.rs @@ -71,6 +71,7 @@ async fn run_snapshot_command(command: &str) -> Result { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run unified exec with shell snapshot".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd, @@ -147,6 +148,7 @@ async fn run_shell_command_snapshot(command: &str) -> Result { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run shell_command with shell snapshot".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd, @@ -284,6 +286,7 @@ async fn shell_command_snapshot_still_intercepts_apply_patch() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "apply patch via shell_command with snapshot".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.clone(), diff --git a/codex-rs/core/tests/suite/skills.rs b/codex-rs/core/tests/suite/skills.rs index ee9343588..eb6b4288d 100644 --- a/codex-rs/core/tests/suite/skills.rs +++ b/codex-rs/core/tests/suite/skills.rs @@ -64,6 +64,7 @@ async fn user_turn_includes_skill_instructions() -> Result<()> { items: vec![ UserInput::Text { text: "please use $demo".to_string(), + text_elements: Vec::new(), }, UserInput::Skill { name: "demo".to_string(), diff --git a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs index b17bb6320..6bfcef38b 100644 --- a/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs +++ b/codex-rs/core/tests/suite/stream_error_allows_next_turn.rs @@ -88,6 +88,7 @@ async fn continue_after_stream_error() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "first message".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) @@ -106,6 +107,7 @@ async fn continue_after_stream_error() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "follow up".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/stream_no_completed.rs b/codex-rs/core/tests/suite/stream_no_completed.rs index 3aa20c0c7..a4962d497 100644 --- a/codex-rs/core/tests/suite/stream_no_completed.rs +++ b/codex-rs/core/tests/suite/stream_no_completed.rs @@ -95,6 +95,7 @@ async fn retries_on_early_close() { .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/tool_harness.rs b/codex-rs/core/tests/suite/tool_harness.rs index e7cd43ca6..75d707af5 100644 --- a/codex-rs/core/tests/suite/tool_harness.rs +++ b/codex-rs/core/tests/suite/tool_harness.rs @@ -82,6 +82,7 @@ async fn shell_tool_executes_command_and_streams_output() -> anyhow::Result<()> .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please run the shell command".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -148,6 +149,7 @@ async fn update_plan_tool_emits_plan_update_event() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please update the plan".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -224,6 +226,7 @@ async fn update_plan_tool_rejects_malformed_payload() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please update the plan".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -312,6 +315,7 @@ async fn apply_patch_tool_executes_and_emits_patch_events() -> anyhow::Result<() .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please apply a patch".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -408,6 +412,7 @@ async fn apply_patch_reports_parse_diagnostics() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please apply a patch".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/tool_parallelism.rs b/codex-rs/core/tests/suite/tool_parallelism.rs index 7661cb423..4abf85d7d 100644 --- a/codex-rs/core/tests/suite/tool_parallelism.rs +++ b/codex-rs/core/tests/suite/tool_parallelism.rs @@ -38,6 +38,7 @@ async fn run_turn(test: &TestCodex, prompt: &str) -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: prompt.into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: test.cwd.path().to_path_buf(), @@ -353,6 +354,7 @@ async fn shell_tools_start_before_response_completed_when_stream_delayed() -> an .submit(Op::UserTurn { items: vec![UserInput::Text { text: "stream delayed completion".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: test.cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/truncation.rs b/codex-rs/core/tests/suite/truncation.rs index 80204e8e1..a86489bdd 100644 --- a/codex-rs/core/tests/suite/truncation.rs +++ b/codex-rs/core/tests/suite/truncation.rs @@ -536,6 +536,7 @@ async fn mcp_image_output_preserves_image_and_no_text_summary() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "call the rmcp image tool".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: fixture.cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/unified_exec.rs b/codex-rs/core/tests/suite/unified_exec.rs index 33524c121..3a6c728df 100644 --- a/codex-rs/core/tests/suite/unified_exec.rs +++ b/codex-rs/core/tests/suite/unified_exec.rs @@ -200,6 +200,7 @@ async fn unified_exec_intercepts_apply_patch_exec_command() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "apply patch via unified exec".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd, @@ -326,6 +327,7 @@ async fn unified_exec_emits_exec_command_begin_event() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "emit begin event".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -401,6 +403,7 @@ async fn unified_exec_resolves_relative_workdir() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run relative workdir test".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -479,6 +482,7 @@ async fn unified_exec_respects_workdir_override() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run workdir test".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -569,6 +573,7 @@ async fn unified_exec_emits_exec_command_end_event() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "emit end event".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -641,6 +646,7 @@ async fn unified_exec_emits_output_delta_for_exec_command() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "emit delta".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -714,6 +720,7 @@ async fn unified_exec_full_lifecycle_with_background_end_event() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "exercise full unified exec lifecycle".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -840,6 +847,7 @@ async fn unified_exec_emits_terminal_interaction_for_write_stdin() -> Result<()> .submit(Op::UserTurn { items: vec![UserInput::Text { text: "stdin delta".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -973,6 +981,7 @@ async fn unified_exec_terminal_interaction_captures_delayed_output() -> Result<( .submit(Op::UserTurn { items: vec![UserInput::Text { text: "delayed terminal interaction output".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1130,6 +1139,7 @@ async fn unified_exec_emits_one_begin_and_one_end_event() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "check poll event behavior".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1225,6 +1235,7 @@ async fn exec_command_reports_chunk_and_exit_metadata() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run metadata test".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1340,6 +1351,7 @@ async fn unified_exec_defaults_to_pipe() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "check default pipe mode".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1427,6 +1439,7 @@ async fn unified_exec_can_enable_tty() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "check tty enabled".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1505,6 +1518,7 @@ async fn unified_exec_respects_early_exit_notifications() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "watch early exit timing".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1633,6 +1647,7 @@ async fn write_stdin_returns_exit_metadata_and_clears_session() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "test write_stdin exit behavior".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1798,6 +1813,7 @@ async fn unified_exec_emits_end_event_when_session_dies_via_stdin() -> Result<() .submit(Op::UserTurn { items: vec![UserInput::Text { text: "end on exit".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1872,6 +1888,7 @@ async fn unified_exec_closes_long_running_session_at_turn_end() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "close unified exec processes on turn end".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -1990,6 +2007,7 @@ async fn unified_exec_reuses_session_via_stdin() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "run unified exec".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -2122,6 +2140,7 @@ PY .submit(Op::UserTurn { items: vec![UserInput::Text { text: "exercise lag handling".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -2233,6 +2252,7 @@ async fn unified_exec_timeout_and_followup_poll() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "check timeout".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -2326,6 +2346,7 @@ PY .submit(Op::UserTurn { items: vec![UserInput::Text { text: "summarize large output".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -2404,6 +2425,7 @@ async fn unified_exec_runs_under_sandbox() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "summarize large output".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -2506,6 +2528,7 @@ async fn unified_exec_python_prompt_under_seatbelt() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "start python under seatbelt".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -2598,6 +2621,7 @@ async fn unified_exec_runs_on_all_platforms() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "summarize large output".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -2728,6 +2752,7 @@ async fn unified_exec_prunes_exited_sessions_first() -> Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "fill session cache".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), diff --git a/codex-rs/core/tests/suite/user_notification.rs b/codex-rs/core/tests/suite/user_notification.rs index 061725823..69f30bb5a 100644 --- a/codex-rs/core/tests/suite/user_notification.rs +++ b/codex-rs/core/tests/suite/user_notification.rs @@ -60,6 +60,7 @@ echo -n "${@: -1}" > $(dirname "${0}")/notify.txt"#, .submit(Op::UserInput { items: vec![UserInput::Text { text: "hello world".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, }) diff --git a/codex-rs/core/tests/suite/view_image.rs b/codex-rs/core/tests/suite/view_image.rs index 034254f7c..ebbbfc478 100644 --- a/codex-rs/core/tests/suite/view_image.rs +++ b/codex-rs/core/tests/suite/view_image.rs @@ -171,6 +171,7 @@ async fn view_image_tool_attaches_local_image() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please add the screenshot".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -301,6 +302,7 @@ async fn view_image_tool_errors_when_path_is_directory() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please attach the folder".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -373,6 +375,7 @@ async fn view_image_tool_placeholder_for_non_image_files() -> anyhow::Result<()> .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please use the view_image tool to read the json file".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), @@ -464,6 +467,7 @@ async fn view_image_tool_errors_when_file_missing() -> anyhow::Result<()> { .submit(Op::UserTurn { items: vec![UserInput::Text { text: "please attach the missing image".into(), + text_elements: Vec::new(), }], final_output_json_schema: None, cwd: cwd.path().to_path_buf(), diff --git a/codex-rs/debug-client/src/client.rs b/codex-rs/debug-client/src/client.rs index 344dd2020..5267e4815 100644 --- a/codex-rs/debug-client/src/client.rs +++ b/codex-rs/debug-client/src/client.rs @@ -184,7 +184,11 @@ impl AppServerClient { request_id: request_id.clone(), params: TurnStartParams { thread_id: thread_id.to_string(), - input: vec![UserInput::Text { text }], + input: vec![UserInput::Text { + text, + // Plain text conversion has no UI element ranges. + text_elements: Vec::new(), + }], ..Default::default() }, }; diff --git a/codex-rs/exec/src/lib.rs b/codex-rs/exec/src/lib.rs index 13baedc22..131e36ce2 100644 --- a/codex-rs/exec/src/lib.rs +++ b/codex-rs/exec/src/lib.rs @@ -360,6 +360,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any .collect(); items.push(UserInput::Text { text: prompt_text.clone(), + text_elements: Vec::new(), }); let output_schema = load_output_schema(output_schema_path.clone()); ( @@ -378,6 +379,7 @@ pub async fn run_main(cli: Cli, codex_linux_sandbox_exe: Option) -> any .collect(); items.push(UserInput::Text { text: prompt_text.clone(), + text_elements: Vec::new(), }); let output_schema = load_output_schema(output_schema_path); ( diff --git a/codex-rs/mcp-server/src/codex_tool_runner.rs b/codex-rs/mcp-server/src/codex_tool_runner.rs index 4d826f920..6aafdf6de 100644 --- a/codex-rs/mcp-server/src/codex_tool_runner.rs +++ b/codex-rs/mcp-server/src/codex_tool_runner.rs @@ -116,6 +116,7 @@ pub async fn run_codex_tool_session( op: Op::UserInput { items: vec![UserInput::Text { text: initial_prompt.clone(), + text_elements: Vec::new(), }], final_output_json_schema: None, }, @@ -158,7 +159,11 @@ pub async fn run_codex_tool_session_reply( .insert(request_id.clone(), thread_id); if let Err(e) = thread .submit(Op::UserInput { - items: vec![UserInput::Text { text: prompt }], + items: vec![UserInput::Text { + text: prompt, + // Plain text conversion has no UI element ranges. + text_elements: Vec::new(), + }], final_output_json_schema: None, }) .await diff --git a/codex-rs/otel/src/traces/otel_manager.rs b/codex-rs/otel/src/traces/otel_manager.rs index 44d2416ac..2b490bbb3 100644 --- a/codex-rs/otel/src/traces/otel_manager.rs +++ b/codex-rs/otel/src/traces/otel_manager.rs @@ -324,7 +324,7 @@ impl OtelManager { let prompt = items .iter() .flat_map(|item| match item { - UserInput::Text { text } => Some(text.as_str()), + UserInput::Text { text, .. } => Some(text.as_str()), _ => None, }) .collect::(); diff --git a/codex-rs/protocol/src/items.rs b/codex-rs/protocol/src/items.rs index 36ee0be07..9d6b484d9 100644 --- a/codex-rs/protocol/src/items.rs +++ b/codex-rs/protocol/src/items.rs @@ -65,6 +65,10 @@ impl UserMessageItem { EventMsg::UserMessage(UserMessageEvent { message: self.message(), images: Some(self.image_urls()), + // TODO: Thread text element ranges into legacy user message events. + text_elements: Vec::new(), + // TODO: Thread local image paths into legacy user message events. + local_images: Vec::new(), }) } @@ -72,7 +76,7 @@ impl UserMessageItem { self.content .iter() .map(|c| match c { - UserInput::Text { text } => text.clone(), + UserInput::Text { text, .. } => text.clone(), _ => String::new(), }) .collect::>() diff --git a/codex-rs/protocol/src/models.rs b/codex-rs/protocol/src/models.rs index 04f54b1dc..d2c9ddd56 100644 --- a/codex-rs/protocol/src/models.rs +++ b/codex-rs/protocol/src/models.rs @@ -550,7 +550,7 @@ impl From> for ResponseInputItem { content: items .into_iter() .flat_map(|c| match c { - UserInput::Text { text } => vec![ContentItem::InputText { text }], + UserInput::Text { text, .. } => vec![ContentItem::InputText { text }], UserInput::Image { image_url } => vec![ ContentItem::InputText { text: image_open_tag_text(), diff --git a/codex-rs/protocol/src/protocol.rs b/codex-rs/protocol/src/protocol.rs index 2116813e1..6850c2ac1 100644 --- a/codex-rs/protocol/src/protocol.rs +++ b/codex-rs/protocol/src/protocol.rs @@ -1255,8 +1255,19 @@ pub struct AgentMessageEvent { #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] pub struct UserMessageEvent { pub message: String, + /// Image URLs sourced from `UserInput::Image`. These are safe + /// to replay in legacy UI history events and correspond to images sent to + /// the model. #[serde(skip_serializing_if = "Option::is_none")] pub images: Option>, + /// 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. + #[serde(default)] + pub local_images: Vec, + /// UI-defined spans within `message` used to render or persist special elements. + #[serde(default)] + pub text_elements: Vec, } #[derive(Debug, Clone, Deserialize, Serialize, JsonSchema, TS)] @@ -2298,6 +2309,48 @@ mod tests { Ok(()) } + #[test] + fn user_input_text_serializes_empty_text_elements() -> Result<()> { + let input = UserInput::Text { + text: "hello".to_string(), + text_elements: Vec::new(), + }; + + let json_input = serde_json::to_value(input)?; + assert_eq!( + json_input, + json!({ + "type": "text", + "text": "hello", + "text_elements": [], + }) + ); + + Ok(()) + } + + #[test] + fn user_message_event_serializes_empty_metadata_vectors() -> Result<()> { + let event = UserMessageEvent { + message: "hello".to_string(), + images: None, + local_images: Vec::new(), + text_elements: Vec::new(), + }; + + let json_event = serde_json::to_value(event)?; + assert_eq!( + json_event, + json!({ + "message": "hello", + "local_images": [], + "text_elements": [], + }) + ); + + Ok(()) + } + /// Serialize Event to verify that its JSON representation has the expected /// amount of nesting. #[test] diff --git a/codex-rs/protocol/src/user_input.rs b/codex-rs/protocol/src/user_input.rs index 26773e1a1..b9ee693d8 100644 --- a/codex-rs/protocol/src/user_input.rs +++ b/codex-rs/protocol/src/user_input.rs @@ -10,17 +10,19 @@ use ts_rs::TS; pub enum UserInput { Text { text: String, + /// UI-defined spans within `text` that should be treated as special elements. + /// These are byte ranges into the UTF-8 `text` buffer and are used to render + /// or persist rich input markers (e.g., image placeholders) across history + /// and resume without mutating the literal text. + #[serde(default)] + text_elements: Vec, }, /// Pre‑encoded data: URI image. - Image { - image_url: String, - }, + Image { image_url: String }, /// Local image path provided by the user. This will be converted to an /// `Image` variant (base64 data URL) during request serialization. - LocalImage { - path: std::path::PathBuf, - }, + LocalImage { path: std::path::PathBuf }, /// Skill selected by the user (name + path to SKILL.md). Skill { @@ -28,3 +30,28 @@ pub enum UserInput { path: std::path::PathBuf, }, } + +#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, TS, JsonSchema)] +pub struct TextElement { + /// Byte range in the parent `text` buffer that this element occupies. + pub byte_range: ByteRange, + /// Optional human-readable placeholder for the element, displayed in the UI. + pub placeholder: Option, +} + +#[derive(Debug, Clone, Copy, Deserialize, Serialize, PartialEq, Eq, TS, JsonSchema)] +pub struct ByteRange { + /// Start byte offset (inclusive) within the UTF-8 text buffer. + pub start: usize, + /// End byte offset (exclusive) within the UTF-8 text buffer. + pub end: usize, +} + +impl From> for ByteRange { + fn from(range: std::ops::Range) -> Self { + Self { + start: range.start, + end: range.end, + } + } +} diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index 9050b4758..6d9f9dafa 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -2266,7 +2266,11 @@ impl ChatWidget { } if !text.is_empty() { - items.push(UserInput::Text { text: text.clone() }); + // TODO: Thread text element ranges from the composer input. Empty keeps old behavior. + items.push(UserInput::Text { + text: text.clone(), + text_elements: Vec::new(), + }); } if let Some(skills) = self.bottom_pane.skills() { diff --git a/codex-rs/tui/src/chatwidget/tests.rs b/codex-rs/tui/src/chatwidget/tests.rs index 78000adff..2d9d808ed 100644 --- a/codex-rs/tui/src/chatwidget/tests.rs +++ b/codex-rs/tui/src/chatwidget/tests.rs @@ -143,6 +143,8 @@ async fn resumed_initial_messages_render_history() { EventMsg::UserMessage(UserMessageEvent { message: "hello from user".to_string(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "assistant reply".to_string(), diff --git a/codex-rs/tui2/src/chatwidget.rs b/codex-rs/tui2/src/chatwidget.rs index c103e058a..1e38e4483 100644 --- a/codex-rs/tui2/src/chatwidget.rs +++ b/codex-rs/tui2/src/chatwidget.rs @@ -2017,7 +2017,11 @@ impl ChatWidget { } if !text.is_empty() { - items.push(UserInput::Text { text: text.clone() }); + // TODO: Thread text element ranges from the composer input. Empty keeps old behavior. + items.push(UserInput::Text { + text: text.clone(), + text_elements: Vec::new(), + }); } if let Some(skills) = self.bottom_pane.skills() { diff --git a/codex-rs/tui2/src/chatwidget/tests.rs b/codex-rs/tui2/src/chatwidget/tests.rs index 82cf4b0ea..f6cb457bf 100644 --- a/codex-rs/tui2/src/chatwidget/tests.rs +++ b/codex-rs/tui2/src/chatwidget/tests.rs @@ -132,6 +132,8 @@ async fn resumed_initial_messages_render_history() { EventMsg::UserMessage(UserMessageEvent { message: "hello from user".to_string(), images: None, + text_elements: Vec::new(), + local_images: Vec::new(), }), EventMsg::AgentMessage(AgentMessageEvent { message: "assistant reply".to_string(),