Persist text element ranges and attached images across history/resume (#9116)

**Summary**
- Backtrack selection now rehydrates `text_elements` and
`local_image_paths` from the chosen user history cell so Esc‑Esc history
edits preserve image placeholders and attachments.
- Composer prefill uses the preserved elements/attachments in both `tui`
and `tui2`.
- Extended backtrack selection tests to cover image placeholder elements
and local image paths.

**Changes**
- `tui/src/app_backtrack.rs`: Backtrack selection now carries text
elements + local image paths; composer prefill uses them (removes TODO).
- `tui2/src/app_backtrack.rs`: Same as above.
- `tui/src/app.rs`: Updated backtrack test to assert restored
elements/paths.
- `tui2/src/app.rs`: Same test updates.

### The original scope of this PR (threading text elements and image
attachments through the codex harness thoroughly/persistently) was
broken into the following PRs other than this one:

The diff of this PR was reduced by changing types in a starter PR:
https://github.com/openai/codex/pull/9235

Then text element metadata was added to protocol, app server, and core
in this PR: https://github.com/openai/codex/pull/9331

Then the end-to-end flow was completed by wiring TUI/TUI2 input,
history, and restore behavior in
https://github.com/openai/codex/pull/9393

Prompt expansion was supported in this PR:
https://github.com/openai/codex/pull/9518

TextElement optional placeholder field was protected in
https://github.com/openai/codex/pull/9545
This commit is contained in:
charley-oai
2026-01-23 10:18:19 -08:00
committed by GitHub
parent f30f39b28b
commit 935d88b455
3 changed files with 57 additions and 22 deletions

View File

@@ -24,6 +24,7 @@
//! both committed history and in-flight activity without changing flush or coalescing behavior.
use std::any::TypeId;
use std::path::PathBuf;
use std::sync::Arc;
use crate::app::App;
@@ -37,6 +38,7 @@ use codex_core::protocol::ErrorEvent;
use codex_core::protocol::EventMsg;
use codex_core::protocol::Op;
use codex_protocol::ThreadId;
use codex_protocol::user_input::TextElement;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
use crossterm::event::KeyEvent;
@@ -78,6 +80,10 @@ pub(crate) struct BacktrackSelection {
/// This is applied immediately on selection confirmation; if the rollback fails, the prefill
/// remains as a convenience so the user can retry or edit.
pub(crate) prefill: String,
/// Text elements associated with the selected user message.
pub(crate) text_elements: Vec<TextElement>,
/// Local image paths associated with the selected user message.
pub(crate) local_image_paths: Vec<PathBuf>,
}
/// An in-flight rollback requested from core.
@@ -198,16 +204,16 @@ impl App {
}
let prefill = selection.prefill.clone();
let text_elements = selection.text_elements.clone();
let local_image_paths = selection.local_image_paths.clone();
self.backtrack.pending_rollback = Some(PendingBacktrackRollback {
selection,
thread_id: self.chat_widget.thread_id(),
});
self.chat_widget.submit_op(Op::ThreadRollback { num_turns });
if !prefill.is_empty() {
// TODO: Rehydrate text_elements/local_image_paths from the selected user cell so
// backtrack preserves image placeholders and attachments.
if !prefill.is_empty() || !text_elements.is_empty() || !local_image_paths.is_empty() {
self.chat_widget
.set_composer_text(prefill, Vec::new(), Vec::new());
.set_composer_text(prefill, text_elements, local_image_paths);
}
}
@@ -481,15 +487,24 @@ impl App {
return None;
}
let prefill = nth_user_position(&self.transcript_cells, nth_user_message)
.and_then(|idx| self.transcript_cells.get(idx))
.and_then(|cell| cell.as_any().downcast_ref::<UserHistoryCell>())
.map(|c| c.message.clone())
.unwrap_or_default();
let (prefill, text_elements, local_image_paths) =
nth_user_position(&self.transcript_cells, nth_user_message)
.and_then(|idx| self.transcript_cells.get(idx))
.and_then(|cell| cell.as_any().downcast_ref::<UserHistoryCell>())
.map(|cell| {
(
cell.message.clone(),
cell.text_elements.clone(),
cell.local_image_paths.clone(),
)
})
.unwrap_or_else(|| (String::new(), Vec::new(), Vec::new()));
Some(BacktrackSelection {
nth_user_message,
prefill,
text_elements,
local_image_paths,
})
}