mirror of
https://github.com/openai/codex.git
synced 2026-09-13 11:47:17 +00:00
tui: preserve image placeholders in history on submit
- Add parsing of paste to attach real files already merged - Keep submitted image placeholders visible in history via new_user_prompt_with_images - Plumb placeholder strings through BottomPane -> ChatWidget on submit - Keep agent input free of placeholders (send images as LocalImage)
This commit is contained in:
@@ -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<String>,
|
||||
}
|
||||
|
||||
/// 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<std::path::PathBuf> {
|
||||
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<String> {
|
||||
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());
|
||||
}
|
||||
|
||||
|
||||
@@ -315,8 +315,15 @@ impl BottomPane<'_> {
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn take_recent_submission_images(&mut self) -> Vec<std::path::PathBuf> {
|
||||
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<String> {
|
||||
self.composer.take_last_submitted_display()
|
||||
}
|
||||
|
||||
// Removed restart_live_status_with_text – no longer used by the current streaming UI.
|
||||
|
||||
@@ -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<InputItem> = 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;
|
||||
|
||||
Reference in New Issue
Block a user