Tui: fix backtracking (#4020)
Backtracking multiple times could drop earlier turns. We now derive the active user-turn positions from the transcript on demand (keying off the latest session header) instead of caching state. This keeps the replayed context intact during repeated edits and adds a regression test.
This commit is contained in:
@@ -443,11 +443,20 @@ impl App {
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::app_backtrack::BacktrackState;
|
||||
use crate::app_backtrack::user_count;
|
||||
use crate::chatwidget::tests::make_chatwidget_manual_with_sender;
|
||||
use crate::file_search::FileSearchManager;
|
||||
use crate::history_cell::AgentMessageCell;
|
||||
use crate::history_cell::HistoryCell;
|
||||
use crate::history_cell::UserHistoryCell;
|
||||
use crate::history_cell::new_session_info;
|
||||
use codex_core::AuthManager;
|
||||
use codex_core::CodexAuth;
|
||||
use codex_core::ConversationManager;
|
||||
use codex_core::protocol::SessionConfiguredEvent;
|
||||
use codex_protocol::mcp_protocol::ConversationId;
|
||||
use ratatui::prelude::Line;
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use std::sync::atomic::AtomicBool;
|
||||
|
||||
@@ -498,4 +507,66 @@ mod tests {
|
||||
Some(ReasoningEffortConfig::High)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
|
||||
let mut app = make_test_app();
|
||||
|
||||
let user_cell = |text: &str| -> Arc<dyn HistoryCell> {
|
||||
Arc::new(UserHistoryCell {
|
||||
message: text.to_string(),
|
||||
}) as Arc<dyn HistoryCell>
|
||||
};
|
||||
let agent_cell = |text: &str| -> Arc<dyn HistoryCell> {
|
||||
Arc::new(AgentMessageCell::new(
|
||||
vec![Line::from(text.to_string())],
|
||||
true,
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
|
||||
let make_header = |is_first| {
|
||||
let event = SessionConfiguredEvent {
|
||||
session_id: ConversationId::new(),
|
||||
model: "gpt-test".to_string(),
|
||||
reasoning_effort: None,
|
||||
history_log_id: 0,
|
||||
history_entry_count: 0,
|
||||
initial_messages: None,
|
||||
rollout_path: PathBuf::new(),
|
||||
};
|
||||
Arc::new(new_session_info(
|
||||
app.chat_widget.config_ref(),
|
||||
event,
|
||||
is_first,
|
||||
)) as Arc<dyn HistoryCell>
|
||||
};
|
||||
|
||||
// Simulate the transcript after trimming for a fork, replaying history, and
|
||||
// appending the edited turn. The session header separates the retained history
|
||||
// from the forked conversation's replayed turns.
|
||||
app.transcript_cells = vec![
|
||||
make_header(true),
|
||||
user_cell("first question"),
|
||||
agent_cell("answer first"),
|
||||
user_cell("follow-up"),
|
||||
agent_cell("answer follow-up"),
|
||||
make_header(false),
|
||||
user_cell("first question"),
|
||||
agent_cell("answer first"),
|
||||
user_cell("follow-up (edited)"),
|
||||
agent_cell("answer edited"),
|
||||
];
|
||||
|
||||
assert_eq!(user_count(&app.transcript_cells), 2);
|
||||
|
||||
app.backtrack.base_id = Some(ConversationId::new());
|
||||
app.backtrack.primed = true;
|
||||
app.backtrack.nth_user_message = user_count(&app.transcript_cells).saturating_sub(1);
|
||||
|
||||
app.confirm_backtrack_from_main();
|
||||
|
||||
let (_, nth, prefill) = app.backtrack.pending.clone().expect("pending backtrack");
|
||||
assert_eq!(nth, 1);
|
||||
assert_eq!(prefill, "follow-up (edited)");
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user