mirror of
https://github.com/openai/codex.git
synced 2026-05-02 02:17:22 +00:00
tui: preserve remote image attachments across resume/backtrack (#10590)
## Summary This PR makes app-server-provided image URLs first-class attachments in TUI, so they survive resume/backtrack/history recall and are resubmitted correctly. <img width="715" height="491" alt="Screenshot 2026-02-12 at 8 27 08 PM" src="https://github.com/user-attachments/assets/226cbd35-8f0c-4e51-a13e-459ef5dd1927" /> Can delete the attached image upon backtracking: <img width="716" height="301" alt="Screenshot 2026-02-12 at 8 27 31 PM" src="https://github.com/user-attachments/assets/4558d230-f1bd-4eed-a093-8e1ab9c6db27" /> In both history and composer, remote images are rendered as normal `[Image #N]` placeholders, with numbering unified with local images. ## What changed - Plumb remote image URLs through TUI message state: - `UserHistoryCell` - `BacktrackSelection` - `ChatComposerHistory::HistoryEntry` - `ChatWidget::UserMessage` - Show remote images as placeholder rows inside the composer box (above textarea), and in history cells. - Support keyboard selection/deletion for remote image rows in composer (`Up`/`Down`, `Delete`/`Backspace`). - Preserve remote-image-only turns in local composer history (Up/Down recall), including restore after backtrack. - Ensure submit/queue/backtrack resubmit include remote images in model input (`UserInput::Image`), and keep request shape stable for remote-image-only turns. - Keep image numbering contiguous across remote + local images: - remote images occupy `[Image #1]..[Image #M]` - local images start at `[Image #M+1]` - deletion renumbers consistently. - In protocol conversion, increment shared image index for remote images too, so mixed remote/local image tags stay in a single sequence. - Simplify restore logic to trust in-memory attachment order (no placeholder-number parsing path). - Backtrack/replay rollback handling now queues trims through `AppEvent::ApplyThreadRollback` and syncs transcript overlay/deferred lines after trims, so overlay/transcript state stays consistent. - Trim trailing blank rendered lines from user history rendering to avoid oversized blank padding. ## Docs + tests - Updated: `docs/tui-chat-composer.md` (remote image flow, selection/deletion, numbering offsets) - Added/updated tests across `tui/src/chatwidget/tests.rs`, `tui/src/app.rs`, `tui/src/app_backtrack.rs`, `tui/src/history_cell.rs`, and `tui/src/bottom_pane/chat_composer.rs` - Added snapshot coverage for remote image composer states, including deleting the first of two remote images. ## Validation - `just fmt` - `cargo test -p codex-tui` ## Codex author `codex fork 019c2636-1571-74a1-8471-15a3b1c3f49d`
This commit is contained in:
committed by
GitHub
parent
395729910c
commit
26a7cd21e2
@@ -250,6 +250,7 @@ impl BottomPane {
|
||||
/// Clear pending attachments and mention bindings e.g. when a slash command doesn't submit text.
|
||||
pub(crate) fn drain_pending_submission_state(&mut self) {
|
||||
let _ = self.take_recent_submission_images_with_placeholders();
|
||||
let _ = self.take_remote_image_urls();
|
||||
let _ = self.take_recent_submission_mention_bindings();
|
||||
let _ = self.take_mention_bindings();
|
||||
}
|
||||
@@ -520,6 +521,21 @@ impl BottomPane {
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn set_remote_image_urls(&mut self, urls: Vec<String>) {
|
||||
self.composer.set_remote_image_urls(urls);
|
||||
self.request_redraw();
|
||||
}
|
||||
|
||||
pub(crate) fn remote_image_urls(&self) -> Vec<String> {
|
||||
self.composer.remote_image_urls()
|
||||
}
|
||||
|
||||
pub(crate) fn take_remote_image_urls(&mut self) -> Vec<String> {
|
||||
let urls = self.composer.take_remote_image_urls();
|
||||
self.request_redraw();
|
||||
urls
|
||||
}
|
||||
|
||||
/// Update the status indicator header (defaults to "Working") and details below it.
|
||||
///
|
||||
/// Passing `None` clears any existing details. No-ops if the status indicator is not active.
|
||||
@@ -1315,6 +1331,58 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn remote_images_render_above_composer_text() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
pane.set_remote_image_urls(vec![
|
||||
"https://example.com/one.png".to_string(),
|
||||
"data:image/png;base64,aGVsbG8=".to_string(),
|
||||
]);
|
||||
|
||||
assert_eq!(pane.composer_text(), "");
|
||||
let width = 48;
|
||||
let height = pane.desired_height(width);
|
||||
let area = Rect::new(0, 0, width, height);
|
||||
let snapshot = render_snapshot(&pane, area);
|
||||
assert!(snapshot.contains("[Image #1]"));
|
||||
assert!(snapshot.contains("[Image #2]"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn drain_pending_submission_state_clears_remote_image_urls() {
|
||||
let (tx_raw, _rx) = unbounded_channel::<AppEvent>();
|
||||
let tx = AppEventSender::new(tx_raw);
|
||||
let mut pane = BottomPane::new(BottomPaneParams {
|
||||
app_event_tx: tx,
|
||||
frame_requester: FrameRequester::test_dummy(),
|
||||
has_input_focus: true,
|
||||
enhanced_keys_supported: false,
|
||||
placeholder_text: "Ask Codex to do anything".to_string(),
|
||||
disable_paste_burst: false,
|
||||
animations_enabled: true,
|
||||
skills: Some(Vec::new()),
|
||||
});
|
||||
|
||||
pane.set_remote_image_urls(vec!["https://example.com/one.png".to_string()]);
|
||||
assert_eq!(pane.remote_image_urls().len(), 1);
|
||||
|
||||
pane.drain_pending_submission_state();
|
||||
|
||||
assert!(pane.remote_image_urls().is_empty());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn esc_with_skill_popup_does_not_interrupt_task() {
|
||||
let (tx_raw, mut rx) = unbounded_channel::<AppEvent>();
|
||||
|
||||
Reference in New Issue
Block a user