diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index 53e88c1eea..f3d1a7f67a 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -1495,7 +1495,8 @@ impl ChatComposer { original_text_elements, original_local_image_paths, ); - self.textarea.set_cursor(original_input.len()); + let cursor = original_input.len().min(self.textarea.text().len()); + self.textarea.set_cursor(cursor); return None; } } @@ -1512,7 +1513,8 @@ impl ChatComposer { original_text_elements, original_local_image_paths, ); - self.textarea.set_cursor(original_input.len()); + let cursor = original_input.len().min(self.textarea.text().len()); + self.textarea.set_cursor(cursor); return None; } }; @@ -1612,7 +1614,8 @@ impl ChatComposer { original_text_elements, original_local_image_paths, ); - self.textarea.set_cursor(original_input.len()); + let cursor = original_input.len().min(self.textarea.text().len()); + self.textarea.set_cursor(cursor); (InputResult::None, true) } } @@ -1860,15 +1863,6 @@ impl ChatComposer { { self.handle_paste(pasted); } - // Backspace at the start of an image placeholder should delete that placeholder (rather - // than deleting content before it). Do this without scanning the full text by consulting - // the textarea's element list. - if matches!(input.code, KeyCode::Backspace) - && self.try_remove_image_element_at_cursor_start() - { - return (InputResult::None, true); - } - // For non-char inputs (or after flushing), handle normally. // Track element removals so we can drop any corresponding placeholders without scanning // the full text. (Placeholders are atomic elements; when deleted, the element disappears.) @@ -1907,29 +1901,6 @@ impl ChatComposer { (InputResult::None, true) } - fn try_remove_image_element_at_cursor_start(&mut self) -> bool { - if self.attached_images.is_empty() { - return false; - } - - let p = self.textarea.cursor(); - let Some(payload) = self.textarea.element_payload_starting_at(p) else { - return false; - }; - let Some(idx) = self - .attached_images - .iter() - .position(|img| img.placeholder == payload) - else { - return false; - }; - - self.textarea.replace_range(p..p + payload.len(), ""); - self.attached_images.remove(idx); - self.relabel_attached_images_and_update_placeholders(); - true - } - fn reconcile_deleted_elements(&mut self, elements_before: Vec) { let elements_after: HashSet = self.textarea.element_payloads().into_iter().collect(); @@ -4144,6 +4115,24 @@ mod tests { composer.attach_image(path.clone()); let placeholder = composer.attached_images[0].placeholder.clone(); + // Case 0: backspace at start should remove the preceding character, not the placeholder. + composer.set_text_content("A".to_string(), Vec::new(), Vec::new()); + composer.attach_image(path.clone()); + let placeholder0 = composer.attached_images[0].placeholder.clone(); + let start0 = composer + .textarea + .text() + .find(&placeholder0) + .expect("placeholder present"); + composer.textarea.set_cursor(start0); + composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)); + assert_eq!(composer.textarea.text(), placeholder0); + assert_eq!(composer.attached_images.len(), 1); + + composer.set_text_content(String::new(), Vec::new(), Vec::new()); + composer.attach_image(path.clone()); + let placeholder = composer.attached_images[0].placeholder.clone(); + // Case 1: backspace at end composer.textarea.move_cursor_to_end_of_line(false); composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)); diff --git a/codex-rs/tui2/src/bottom_pane/chat_composer.rs b/codex-rs/tui2/src/bottom_pane/chat_composer.rs index 6b1e59ef1e..a0517c3fd8 100644 --- a/codex-rs/tui2/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui2/src/bottom_pane/chat_composer.rs @@ -1428,7 +1428,8 @@ impl ChatComposer { original_text_elements, original_local_image_paths, ); - self.textarea.set_cursor(original_input.len()); + let cursor = original_input.len().min(self.textarea.text().len()); + self.textarea.set_cursor(cursor); return None; } } @@ -1445,7 +1446,8 @@ impl ChatComposer { original_text_elements, original_local_image_paths, ); - self.textarea.set_cursor(original_input.len()); + let cursor = original_input.len().min(self.textarea.text().len()); + self.textarea.set_cursor(cursor); return None; } }; @@ -1545,7 +1547,8 @@ impl ChatComposer { original_text_elements, original_local_image_paths, ); - self.textarea.set_cursor(original_input.len()); + let cursor = original_input.len().min(self.textarea.text().len()); + self.textarea.set_cursor(cursor); (InputResult::None, true) } } @@ -1794,15 +1797,6 @@ impl ChatComposer { self.handle_paste(pasted); } - // Backspace at the start of an image placeholder should delete that placeholder (rather - // than deleting content before it). Do this without scanning the full text by consulting - // the textarea's element list. - if matches!(input.code, KeyCode::Backspace) - && self.try_remove_image_element_at_cursor_start() - { - return (InputResult::None, true); - } - // Track element removals so we can drop any corresponding placeholders without scanning // the full text. (Placeholders are atomic elements; when deleted, the element disappears.) let elements_before = if self.pending_pastes.is_empty() && self.attached_images.is_empty() { @@ -1840,29 +1834,6 @@ impl ChatComposer { (InputResult::None, true) } - fn try_remove_image_element_at_cursor_start(&mut self) -> bool { - if self.attached_images.is_empty() { - return false; - } - - let p = self.textarea.cursor(); - let Some(payload) = self.textarea.element_payload_starting_at(p) else { - return false; - }; - let Some(idx) = self - .attached_images - .iter() - .position(|img| img.placeholder == payload) - else { - return false; - }; - - self.textarea.replace_range(p..p + payload.len(), ""); - self.attached_images.remove(idx); - self.relabel_attached_images_and_update_placeholders(); - true - } - fn reconcile_deleted_elements(&mut self, elements_before: Vec) { let elements_after: HashSet = self.textarea.element_payloads().into_iter().collect(); @@ -4112,6 +4083,24 @@ mod tests { composer.attach_image(path.clone()); let placeholder = composer.attached_images[0].placeholder.clone(); + // Case 0: backspace at start should remove the preceding character, not the placeholder. + composer.set_text_content("A".to_string(), Vec::new(), Vec::new()); + composer.attach_image(path.clone()); + let placeholder0 = composer.attached_images[0].placeholder.clone(); + let start0 = composer + .textarea + .text() + .find(&placeholder0) + .expect("placeholder present"); + composer.textarea.set_cursor(start0); + composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE)); + assert_eq!(composer.textarea.text(), placeholder0); + assert_eq!(composer.attached_images.len(), 1); + + composer.set_text_content(String::new(), Vec::new(), Vec::new()); + composer.attach_image(path.clone()); + let placeholder = composer.attached_images[0].placeholder.clone(); + // Case 1: backspace at end composer.textarea.move_cursor_to_end_of_line(false); composer.handle_key_event(KeyEvent::new(KeyCode::Backspace, KeyModifiers::NONE));