diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index e2b18a7896..43a6c3ed4a 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -72,6 +72,7 @@ pub(crate) struct ChatComposer { has_focus: bool, attached_images: Vec<(String, std::path::PathBuf)>, placeholder_text: String, + last_submitted_display: Option, } /// Popup state – at most one can be visible at any time. @@ -105,6 +106,7 @@ impl ChatComposer { has_focus: has_input_focus, attached_images: Vec::new(), placeholder_text, + last_submitted_display: None, } } @@ -210,9 +212,15 @@ impl ChatComposer { true } - pub fn take_recent_submission_images(&mut self) -> Vec { + pub fn take_recent_submission_images_with_placeholders( + &mut self, + ) -> Vec<(String, std::path::PathBuf)> { let images = std::mem::take(&mut self.attached_images); - images.into_iter().map(|(_, path)| path).collect() + images + } + + pub fn take_last_submitted_display(&mut self) -> Option { + self.last_submitted_display.take() } /// Integrate results from an asynchronous file search. @@ -605,18 +613,23 @@ impl ChatComposer { modifiers: KeyModifiers::NONE, .. } => { - let mut text = self.textarea.text().to_string(); - self.textarea.set_text(""); + // Build display string that preserves inline image placeholders + let mut display = self.textarea.text().to_string(); + for (placeholder, actual) in &self.pending_pastes { + if display.contains(placeholder) { + display = display.replace(placeholder, actual); + } + } + self.last_submitted_display = Some(display); - // Replace all pending pastes in the text + // Build the agent text: remove image placeholders, trim + let mut text = self.textarea.text().to_string(); for (placeholder, actual) in &self.pending_pastes { if text.contains(placeholder) { text = text.replace(placeholder, actual); } } self.pending_pastes.clear(); - - // Strip image placeholders from the submitted text; images are retrieved via take_recent_submission_images() for (placeholder, _) in &self.attached_images { if text.contains(placeholder) { text = text.replace(placeholder, ""); @@ -627,7 +640,8 @@ impl ChatComposer { if !text.is_empty() { self.history.record_local_submission(&text); } - // Do not clear attached_images here; ChatWidget drains them via take_recent_submission_images(). + // Clear textarea after capturing content + self.textarea.set_text(""); (InputResult::Submitted(text), true) } input => self.handle_input_basic(input), @@ -1489,9 +1503,9 @@ mod tests { InputResult::Submitted(text) => assert_eq!(text, "hi"), _ => panic!("expected Submitted"), } - let imgs = composer.take_recent_submission_images(); + let imgs = composer.take_recent_submission_images_with_placeholders(); assert_eq!(imgs.len(), 1); - assert_eq!(imgs[0], path); + assert_eq!(imgs[0].1, path); } #[test] @@ -1511,9 +1525,9 @@ mod tests { InputResult::Submitted(text) => assert!(text.is_empty()), _ => panic!("expected Submitted"), } - let imgs = composer.take_recent_submission_images(); + let imgs = composer.take_recent_submission_images_with_placeholders(); assert_eq!(imgs.len(), 1); - assert_eq!(imgs[0], path); + assert_eq!(imgs[0].1, path); assert!(composer.attached_images.is_empty()); } diff --git a/codex-rs/tui/src/bottom_pane/mod.rs b/codex-rs/tui/src/bottom_pane/mod.rs index 9bb3715cd8..a212cba99a 100644 --- a/codex-rs/tui/src/bottom_pane/mod.rs +++ b/codex-rs/tui/src/bottom_pane/mod.rs @@ -315,8 +315,15 @@ impl BottomPane<'_> { } } - pub(crate) fn take_recent_submission_images(&mut self) -> Vec { - self.composer.take_recent_submission_images() + pub(crate) fn take_recent_submission_images_with_placeholders( + &mut self, + ) -> Vec<(String, std::path::PathBuf)> { + self.composer + .take_recent_submission_images_with_placeholders() + } + + pub(crate) fn take_last_submitted_display(&mut self) -> Option { + self.composer.take_last_submitted_display() } // Removed restart_live_status_with_text – no longer used by the current streaming UI. diff --git a/codex-rs/tui/src/chatwidget.rs b/codex-rs/tui/src/chatwidget.rs index a9529854eb..99e4a22d38 100644 --- a/codex-rs/tui/src/chatwidget.rs +++ b/codex-rs/tui/src/chatwidget.rs @@ -543,11 +543,14 @@ impl ChatWidget<'_> { match self.bottom_pane.handle_key_event(key_event) { InputResult::Submitted(text) => { - let images = self.bottom_pane.take_recent_submission_images(); - self.submit_user_message(UserMessage { - text, - image_paths: images, - }); + let images = self + .bottom_pane + .take_recent_submission_images_with_placeholders(); + let display = self + .bottom_pane + .take_last_submitted_display() + .unwrap_or_else(|| text.clone()); + self.submit_user_message_with_display(text, images, display); } InputResult::None => {} } @@ -628,6 +631,38 @@ impl ChatWidget<'_> { } } + fn submit_user_message_with_display( + &mut self, + text: String, + images: Vec<(String, std::path::PathBuf)>, + display_text: String, + ) { + let mut items: Vec = Vec::new(); + if !text.is_empty() { + items.push(InputItem::Text { text: text.clone() }); + } + for (_, path) in &images { + items.push(InputItem::LocalImage { path: path.clone() }); + } + if items.is_empty() { + return; + } + self.codex_op_tx + .send(Op::UserInput { items }) + .unwrap_or_else(|e| { + tracing::error!("failed to send message: {e}"); + }); + if !text.is_empty() { + self.codex_op_tx + .send(Op::AddToHistory { text: text.clone() }) + .unwrap_or_else(|e| { + tracing::error!("failed to send AddHistory op: {e}"); + }); + } + // Show the original display text (with inline image placeholders) in history. + self.add_to_history(&history_cell::new_user_prompt(display_text)); + } + pub(crate) fn handle_codex_event(&mut self, event: Event) { // Reset redraw flag for this dispatch self.needs_redraw = false;