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

@@ -2203,6 +2203,7 @@ mod tests {
use codex_core::protocol::SessionSource;
use codex_otel::OtelManager;
use codex_protocol::ThreadId;
use codex_protocol::user_input::TextElement;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use ratatui::prelude::Line;
@@ -2503,11 +2504,14 @@ mod tests {
async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
let user_cell = |text: &str| -> Arc<dyn HistoryCell> {
let user_cell = |text: &str,
text_elements: Vec<TextElement>,
local_image_paths: Vec<PathBuf>|
-> Arc<dyn HistoryCell> {
Arc::new(UserHistoryCell {
message: text.to_string(),
text_elements: Vec::new(),
local_image_paths: Vec::new(),
text_elements,
local_image_paths,
}) as Arc<dyn HistoryCell>
};
let agent_cell = |text: &str| -> Arc<dyn HistoryCell> {
@@ -2545,18 +2549,28 @@ mod tests {
)) as Arc<dyn HistoryCell>
};
let placeholder = "[Image #1]";
let edited_text = format!("follow-up (edited) {placeholder}");
let edited_range = edited_text.len().saturating_sub(placeholder.len())..edited_text.len();
let edited_text_elements = vec![TextElement::new(edited_range.into(), None)];
let edited_local_image_paths = vec![PathBuf::from("/tmp/fake-image.png")];
// Simulate a transcript with duplicated history (e.g., from prior backtracks)
// and an edited turn appended after a session header boundary.
app.transcript_cells = vec![
make_header(true),
user_cell("first question"),
user_cell("first question", Vec::new(), Vec::new()),
agent_cell("answer first"),
user_cell("follow-up"),
user_cell("follow-up", Vec::new(), Vec::new()),
agent_cell("answer follow-up"),
make_header(false),
user_cell("first question"),
user_cell("first question", Vec::new(), Vec::new()),
agent_cell("answer first"),
user_cell("follow-up (edited)"),
user_cell(
&edited_text,
edited_text_elements.clone(),
edited_local_image_paths.clone(),
),
agent_cell("answer edited"),
];
@@ -2589,7 +2603,9 @@ mod tests {
.confirm_backtrack_from_main()
.expect("backtrack selection");
assert_eq!(selection.nth_user_message, 1);
assert_eq!(selection.prefill, "follow-up (edited)");
assert_eq!(selection.prefill, edited_text);
assert_eq!(selection.text_elements, edited_text_elements);
assert_eq!(selection.local_image_paths, edited_local_image_paths);
app.apply_backtrack_rollback(selection);