Use thread rollback for Esc backtrack (#9140)

- Swap Esc backtrack to roll back the current thread instead of forking
This commit is contained in:
Ahmed Ibrahim 2026-01-13 01:17:39 -08:00 committed by GitHub
parent 17ab5f6a52
commit 3a300d1117
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
6 changed files with 196 additions and 277 deletions

View file

@ -813,9 +813,6 @@ impl App {
}
self.chat_widget.handle_codex_event(event);
}
AppEvent::ConversationHistory(ev) => {
self.on_conversation_history_for_backtrack(tui, ev).await?;
}
AppEvent::ExitRequest => {
return Ok(false);
}
@ -1437,8 +1434,9 @@ impl App {
&& self.backtrack.nth_user_message != usize::MAX
&& self.chat_widget.composer_is_empty() =>
{
// Delegate to helper for clarity; preserves behavior.
self.confirm_backtrack_from_main();
if let Some(selection) = self.confirm_backtrack_from_main() {
self.apply_backtrack_selection(tui, selection);
}
}
KeyEvent {
kind: KeyEventKind::Press | KeyEventKind::Repeat,
@ -1509,6 +1507,7 @@ mod tests {
use codex_core::protocol::SessionConfiguredEvent;
use codex_protocol::ThreadId;
use insta::assert_snapshot;
use pretty_assertions::assert_eq;
use ratatui::prelude::Line;
use std::path::PathBuf;
use std::sync::Arc;
@ -1775,7 +1774,7 @@ mod tests {
#[tokio::test]
async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
let mut app = make_test_app().await;
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
let user_cell = |text: &str| -> Arc<dyn HistoryCell> {
Arc::new(UserHistoryCell {
@ -1811,9 +1810,8 @@ mod tests {
)) 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 thread's replayed turns.
// 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"),
@ -1829,15 +1827,44 @@ mod tests {
assert_eq!(user_count(&app.transcript_cells), 2);
app.backtrack.base_id = Some(ThreadId::new());
let base_id = ThreadId::new();
app.chat_widget.handle_codex_event(Event {
id: String::new(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: base_id,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::ReadOnly,
cwd: PathBuf::from("/home/user/project"),
reasoning_effort: None,
history_log_id: 0,
history_entry_count: 0,
initial_messages: None,
rollout_path: PathBuf::new(),
}),
});
app.backtrack.base_id = Some(base_id);
app.backtrack.primed = true;
app.backtrack.nth_user_message = user_count(&app.transcript_cells).saturating_sub(1);
app.confirm_backtrack_from_main();
let selection = app
.confirm_backtrack_from_main()
.expect("backtrack selection");
assert_eq!(selection.nth_user_message, 1);
assert_eq!(selection.prefill, "follow-up (edited)");
let (_, nth, prefill) = app.backtrack.pending.clone().expect("pending backtrack");
assert_eq!(nth, 1);
assert_eq!(prefill, "follow-up (edited)");
app.apply_backtrack_rollback(selection);
let mut rollback_turns = None;
while let Ok(op) = op_rx.try_recv() {
if let Op::ThreadRollback { num_turns } = op {
rollback_turns = Some(num_turns);
}
}
assert_eq!(rollback_turns, Some(1));
}
#[tokio::test]

View file

@ -12,7 +12,6 @@
//! 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;
@ -21,7 +20,7 @@ use crate::history_cell::UserHistoryCell;
use crate::pager_overlay::Overlay;
use crate::tui;
use crate::tui::TuiEvent;
use codex_core::protocol::ConversationPathResponseEvent;
use codex_core::protocol::Op;
use codex_protocol::ThreadId;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
@ -33,14 +32,18 @@ use crossterm::event::KeyEventKind;
pub(crate) struct BacktrackState {
/// True when Esc has primed backtrack mode in the main view.
pub(crate) primed: bool,
/// Session id of the base thread to fork from.
/// Session id of the base thread to rollback.
pub(crate) base_id: Option<ThreadId>,
/// Index in the transcript of the last user message.
pub(crate) nth_user_message: usize,
/// True when the transcript overlay is showing a backtrack preview.
pub(crate) overlay_preview_active: bool,
/// Pending fork request: (base_id, nth_user_message, prefill).
pub(crate) pending: Option<(ThreadId, usize, String)>,
}
#[derive(Debug, Clone)]
pub(crate) struct BacktrackSelection {
pub(crate) nth_user_message: usize,
pub(crate) prefill: String,
}
impl App {
@ -109,22 +112,22 @@ impl App {
}
/// Stage a backtrack and request thread history from the agent.
pub(crate) fn request_backtrack(
&mut self,
prefill: String,
base_id: ThreadId,
nth_user_message: usize,
) {
self.backtrack.pending = Some((base_id, nth_user_message, prefill));
if let Some(path) = self.chat_widget.rollout_path() {
let ev = ConversationPathResponseEvent {
conversation_id: base_id,
path,
};
self.app_event_tx
.send(crate::app_event::AppEvent::ConversationHistory(ev));
} else {
tracing::error!("rollout path unavailable; cannot backtrack");
pub(crate) fn apply_backtrack_rollback(&mut self, selection: BacktrackSelection) {
let user_total = user_count(&self.transcript_cells);
if user_total == 0 {
return;
}
let num_turns = user_total.saturating_sub(selection.nth_user_message);
let num_turns = u32::try_from(num_turns).unwrap_or(u32::MAX);
if num_turns == 0 {
return;
}
self.chat_widget.submit_op(Op::ThreadRollback { num_turns });
self.trim_transcript_for_backtrack(selection.nth_user_message);
if !selection.prefill.is_empty() {
self.chat_widget.set_composer_text(selection.prefill);
}
}
@ -186,7 +189,7 @@ impl App {
self.backtrack.overlay_preview_active = true;
let count = user_count(&self.transcript_cells);
if let Some(last) = count.checked_sub(1) {
self.apply_backtrack_selection(last);
self.apply_backtrack_selection_internal(last);
}
tui.frame_requester().schedule_frame();
}
@ -210,12 +213,12 @@ impl App {
.min(last_index)
};
self.apply_backtrack_selection(next_selection);
self.apply_backtrack_selection_internal(next_selection);
tui.frame_requester().schedule_frame();
}
/// Apply a computed backtrack selection to the overlay and internal counter.
fn apply_backtrack_selection(&mut self, nth_user_message: usize) {
fn apply_backtrack_selection_internal(&mut self, nth_user_message: usize) {
if let Some(cell_idx) = nth_user_position(&self.transcript_cells, nth_user_message) {
self.backtrack.nth_user_message = nth_user_message;
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
@ -283,16 +286,13 @@ impl App {
/// Handle Enter in overlay backtrack preview: confirm selection and reset state.
fn overlay_confirm_backtrack(&mut self, tui: &mut tui::Tui) {
let nth_user_message = self.backtrack.nth_user_message;
if let Some(base_id) = self.backtrack.base_id {
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();
self.close_transcript_overlay(tui);
self.request_backtrack(prefill, base_id, nth_user_message);
let selection = self.backtrack_selection(nth_user_message);
self.close_transcript_overlay(tui);
if let Some(selection) = selection {
self.apply_backtrack_rollback(selection);
self.render_transcript_once(tui);
tui.frame_requester().schedule_frame();
}
self.reset_backtrack_state();
}
/// Handle Esc in overlay backtrack preview: step selection if armed, else forward.
@ -306,18 +306,11 @@ impl App {
}
/// Confirm a primed backtrack from the main view (no overlay visible).
/// Computes the prefill from the selected user message and requests history.
pub(crate) fn confirm_backtrack_from_main(&mut self) {
if let Some(base_id) = self.backtrack.base_id {
let prefill =
nth_user_position(&self.transcript_cells, self.backtrack.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();
self.request_backtrack(prefill, base_id, self.backtrack.nth_user_message);
}
/// Computes the prefill from the selected user message for rollback.
pub(crate) fn confirm_backtrack_from_main(&mut self) -> Option<BacktrackSelection> {
let selection = self.backtrack_selection(self.backtrack.nth_user_message);
self.reset_backtrack_state();
selection
}
/// Clear all backtrack-related state and composer hints.
@ -329,89 +322,34 @@ impl App {
self.chat_widget.clear_esc_backtrack_hint();
}
/// Handle a ConversationHistory response while a backtrack is pending.
/// If it matches the primed base session, fork and switch to the new conversation.
pub(crate) async fn on_conversation_history_for_backtrack(
pub(crate) fn apply_backtrack_selection(
&mut self,
tui: &mut tui::Tui,
ev: ConversationPathResponseEvent,
) -> Result<()> {
if let Some((base_id, _, _)) = self.backtrack.pending.as_ref()
&& ev.conversation_id == *base_id
&& let Some((_, nth_user_message, prefill)) = self.backtrack.pending.take()
{
self.fork_and_switch_to_new_conversation(tui, ev, nth_user_message, prefill)
.await;
}
Ok(())
}
/// Fork the conversation using provided history and switch UI/state accordingly.
async fn fork_and_switch_to_new_conversation(
&mut self,
tui: &mut tui::Tui,
ev: ConversationPathResponseEvent,
nth_user_message: usize,
prefill: String,
selection: BacktrackSelection,
) {
let cfg = self.chat_widget.config_ref().clone();
// Perform the fork via a thin wrapper for clarity/testability.
let result = self
.perform_fork(ev.path.clone(), nth_user_message, cfg.clone())
.await;
match result {
Ok(new_conv) => {
self.install_forked_conversation(tui, cfg, new_conv, nth_user_message, &prefill)
}
Err(e) => tracing::error!("error forking conversation: {e:#}"),
}
}
/// Thin wrapper around ThreadManager::fork_thread.
async fn perform_fork(
&self,
path: PathBuf,
nth_user_message: usize,
cfg: codex_core::config::Config,
) -> codex_core::error::Result<codex_core::NewThread> {
self.server.fork_thread(nth_user_message, cfg, path).await
}
/// Install a forked thread into the ChatWidget and update UI to reflect selection.
fn install_forked_conversation(
&mut self,
tui: &mut tui::Tui,
cfg: codex_core::config::Config,
new_conv: codex_core::NewThread,
nth_user_message: usize,
prefill: &str,
) {
let thread = new_conv.thread;
let session_configured = new_conv.session_configured;
let init = crate::chatwidget::ChatWidgetInit {
config: cfg,
model: self.current_model.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
initial_images: Vec::new(),
enhanced_keys_supported: self.enhanced_keys_supported,
auth_manager: self.auth_manager.clone(),
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
};
self.chat_widget =
crate::chatwidget::ChatWidget::new_from_existing(init, thread, session_configured);
// Trim transcript up to the selected user message and re-render it.
self.trim_transcript_for_backtrack(nth_user_message);
self.apply_backtrack_rollback(selection);
self.render_transcript_once(tui);
if !prefill.is_empty() {
self.chat_widget.set_composer_text(prefill.to_string());
}
tui.frame_requester().schedule_frame();
}
fn backtrack_selection(&self, nth_user_message: usize) -> Option<BacktrackSelection> {
let base_id = self.backtrack.base_id?;
if self.chat_widget.thread_id() != Some(base_id) {
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();
Some(BacktrackSelection {
nth_user_message,
prefill,
})
}
/// Trim transcript_cells to preserve only content up to the selected user message.
fn trim_transcript_for_backtrack(&mut self, nth_user_message: usize) {
trim_transcript_cells_to_nth_user(&mut self.transcript_cells, nth_user_message);

View file

@ -1,7 +1,6 @@
use std::path::PathBuf;
use codex_common::approval_presets::ApprovalPreset;
use codex_core::protocol::ConversationPathResponseEvent;
use codex_core::protocol::Event;
use codex_core::protocol::RateLimitSnapshot;
use codex_file_search::FileMatch;
@ -186,9 +185,6 @@ pub(crate) enum AppEvent {
/// Re-open the approval presets popup.
OpenApprovalsPopup,
/// Forwarded conversation history snapshot from the current conversation.
ConversationHistory(ConversationPathResponseEvent),
/// Open the branch picker option from the review popup.
OpenReviewBranchPicker(PathBuf),

View file

@ -1594,9 +1594,6 @@ impl App {
}
self.chat_widget.handle_codex_event(event);
}
AppEvent::ConversationHistory(ev) => {
self.on_conversation_history_for_backtrack(tui, ev).await?;
}
AppEvent::ExitRequest => {
return Ok(false);
}
@ -2194,8 +2191,9 @@ impl App {
&& self.backtrack.nth_user_message != usize::MAX
&& self.chat_widget.composer_is_empty() =>
{
// Delegate to helper for clarity; preserves behavior.
self.confirm_backtrack_from_main();
if let Some(selection) = self.confirm_backtrack_from_main() {
self.apply_backtrack_selection(tui, selection);
}
}
KeyEvent {
kind: KeyEventKind::Press | KeyEventKind::Repeat,
@ -2564,7 +2562,7 @@ mod tests {
#[tokio::test]
async fn backtrack_selection_with_duplicate_history_targets_unique_turn() {
let mut app = make_test_app().await;
let (mut app, _app_event_rx, mut op_rx) = make_test_app_with_channels().await;
let user_cell = |text: &str| -> Arc<dyn HistoryCell> {
Arc::new(UserHistoryCell {
@ -2600,9 +2598,8 @@ mod tests {
)) 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.
// 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"),
@ -2618,15 +2615,44 @@ mod tests {
assert_eq!(user_count(&app.transcript_cells), 2);
app.backtrack.base_id = Some(ThreadId::new());
let base_id = ThreadId::new();
app.chat_widget.handle_codex_event(Event {
id: String::new(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: base_id,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
approval_policy: AskForApproval::Never,
sandbox_policy: SandboxPolicy::ReadOnly,
cwd: PathBuf::from("/home/user/project"),
reasoning_effort: None,
history_log_id: 0,
history_entry_count: 0,
initial_messages: None,
rollout_path: PathBuf::new(),
}),
});
app.backtrack.base_id = Some(base_id);
app.backtrack.primed = true;
app.backtrack.nth_user_message = user_count(&app.transcript_cells).saturating_sub(1);
app.confirm_backtrack_from_main();
let selection = app
.confirm_backtrack_from_main()
.expect("backtrack selection");
assert_eq!(selection.nth_user_message, 1);
assert_eq!(selection.prefill, "follow-up (edited)");
let (_, nth, prefill) = app.backtrack.pending.clone().expect("pending backtrack");
assert_eq!(nth, 1);
assert_eq!(prefill, "follow-up (edited)");
app.apply_backtrack_rollback(selection);
let mut rollback_turns = None;
while let Ok(op) = op_rx.try_recv() {
if let Op::ThreadRollback { num_turns } = op {
rollback_turns = Some(num_turns);
}
}
assert_eq!(rollback_turns, Some(1));
}
#[tokio::test]

View file

@ -12,7 +12,6 @@
//! 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;
@ -21,7 +20,7 @@ use crate::history_cell::UserHistoryCell;
use crate::pager_overlay::Overlay;
use crate::tui;
use crate::tui::TuiEvent;
use codex_core::protocol::ConversationPathResponseEvent;
use codex_core::protocol::Op;
use codex_protocol::ThreadId;
use color_eyre::eyre::Result;
use crossterm::event::KeyCode;
@ -33,14 +32,18 @@ use crossterm::event::KeyEventKind;
pub(crate) struct BacktrackState {
/// True when Esc has primed backtrack mode in the main view.
pub(crate) primed: bool,
/// Session id of the base conversation to fork from.
/// Session id of the base thread to rollback.
pub(crate) base_id: Option<ThreadId>,
/// Index in the transcript of the last user message.
pub(crate) nth_user_message: usize,
/// True when the transcript overlay is showing a backtrack preview.
pub(crate) overlay_preview_active: bool,
/// Pending fork request: (base_id, nth_user_message, prefill).
pub(crate) pending: Option<(ThreadId, usize, String)>,
}
#[derive(Debug, Clone)]
pub(crate) struct BacktrackSelection {
pub(crate) nth_user_message: usize,
pub(crate) prefill: String,
}
impl App {
@ -108,23 +111,22 @@ impl App {
}
}
/// Stage a backtrack and request conversation history from the agent.
pub(crate) fn request_backtrack(
&mut self,
prefill: String,
base_id: ThreadId,
nth_user_message: usize,
) {
self.backtrack.pending = Some((base_id, nth_user_message, prefill));
if let Some(path) = self.chat_widget.rollout_path() {
let ev = ConversationPathResponseEvent {
conversation_id: base_id,
path,
};
self.app_event_tx
.send(crate::app_event::AppEvent::ConversationHistory(ev));
} else {
tracing::error!("rollout path unavailable; cannot backtrack");
pub(crate) fn apply_backtrack_rollback(&mut self, selection: BacktrackSelection) {
let user_total = user_count(&self.transcript_cells);
if user_total == 0 {
return;
}
let num_turns = user_total.saturating_sub(selection.nth_user_message);
let num_turns = u32::try_from(num_turns).unwrap_or(u32::MAX);
if num_turns == 0 {
return;
}
self.chat_widget.submit_op(Op::ThreadRollback { num_turns });
self.trim_transcript_for_backtrack(selection.nth_user_message);
if !selection.prefill.is_empty() {
self.chat_widget.set_composer_text(selection.prefill);
}
}
@ -216,7 +218,7 @@ impl App {
self.backtrack.overlay_preview_active = true;
let count = user_count(&self.transcript_cells);
if let Some(last) = count.checked_sub(1) {
self.apply_backtrack_selection(last);
self.apply_backtrack_selection_internal(last);
}
tui.frame_requester().schedule_frame();
}
@ -240,12 +242,12 @@ impl App {
.min(last_index)
};
self.apply_backtrack_selection(next_selection);
self.apply_backtrack_selection_internal(next_selection);
tui.frame_requester().schedule_frame();
}
/// Apply a computed backtrack selection to the overlay and internal counter.
fn apply_backtrack_selection(&mut self, nth_user_message: usize) {
fn apply_backtrack_selection_internal(&mut self, nth_user_message: usize) {
if let Some(cell_idx) = nth_user_position(&self.transcript_cells, nth_user_message) {
self.backtrack.nth_user_message = nth_user_message;
if let Some(Overlay::Transcript(t)) = &mut self.overlay {
@ -305,16 +307,13 @@ impl App {
/// Handle Enter in overlay backtrack preview: confirm selection and reset state.
fn overlay_confirm_backtrack(&mut self, tui: &mut tui::Tui) {
let nth_user_message = self.backtrack.nth_user_message;
if let Some(base_id) = self.backtrack.base_id {
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();
self.close_transcript_overlay(tui);
self.request_backtrack(prefill, base_id, nth_user_message);
let selection = self.backtrack_selection(nth_user_message);
self.close_transcript_overlay(tui);
if let Some(selection) = selection {
self.apply_backtrack_rollback(selection);
self.render_transcript_once(tui);
tui.frame_requester().schedule_frame();
}
self.reset_backtrack_state();
}
/// Handle Esc in overlay backtrack preview: step selection if armed, else forward.
@ -328,18 +327,11 @@ impl App {
}
/// Confirm a primed backtrack from the main view (no overlay visible).
/// Computes the prefill from the selected user message and requests history.
pub(crate) fn confirm_backtrack_from_main(&mut self) {
if let Some(base_id) = self.backtrack.base_id {
let prefill =
nth_user_position(&self.transcript_cells, self.backtrack.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();
self.request_backtrack(prefill, base_id, self.backtrack.nth_user_message);
}
/// Computes the prefill from the selected user message for rollback.
pub(crate) fn confirm_backtrack_from_main(&mut self) -> Option<BacktrackSelection> {
let selection = self.backtrack_selection(self.backtrack.nth_user_message);
self.reset_backtrack_state();
selection
}
/// Clear all backtrack-related state and composer hints.
@ -351,88 +343,32 @@ impl App {
self.chat_widget.clear_esc_backtrack_hint();
}
/// Handle a ConversationHistory response while a backtrack is pending.
/// If it matches the primed base session, fork and switch to the new thread.
pub(crate) async fn on_conversation_history_for_backtrack(
pub(crate) fn apply_backtrack_selection(
&mut self,
tui: &mut tui::Tui,
ev: ConversationPathResponseEvent,
) -> Result<()> {
if let Some((base_id, _, _)) = self.backtrack.pending.as_ref()
&& ev.conversation_id == *base_id
&& let Some((_, nth_user_message, prefill)) = self.backtrack.pending.take()
{
self.fork_and_switch_to_new_conversation(tui, ev, nth_user_message, prefill)
.await;
}
Ok(())
}
/// Fork the thread using provided history and switch UI/state accordingly.
async fn fork_and_switch_to_new_conversation(
&mut self,
tui: &mut tui::Tui,
ev: ConversationPathResponseEvent,
nth_user_message: usize,
prefill: String,
selection: BacktrackSelection,
) {
let cfg = self.chat_widget.config_ref().clone();
// Perform the fork via a thin wrapper for clarity/testability.
let result = self
.perform_fork(ev.path.clone(), nth_user_message, cfg.clone())
.await;
match result {
Ok(new_conv) => {
self.install_forked_conversation(tui, cfg, new_conv, nth_user_message, &prefill)
}
Err(e) => tracing::error!("error forking conversation: {e:#}"),
}
}
/// Thin wrapper around ThreadManager::fork_thread.
async fn perform_fork(
&self,
path: PathBuf,
nth_user_message: usize,
cfg: codex_core::config::Config,
) -> codex_core::error::Result<codex_core::NewThread> {
self.server.fork_thread(nth_user_message, cfg, path).await
}
/// Install a forked thread into the ChatWidget and update UI to reflect selection.
fn install_forked_conversation(
&mut self,
tui: &mut tui::Tui,
cfg: codex_core::config::Config,
new_conv: codex_core::NewThread,
nth_user_message: usize,
prefill: &str,
) {
let thread = new_conv.thread;
let session_configured = new_conv.session_configured;
let init = crate::chatwidget::ChatWidgetInit {
config: cfg,
model: self.current_model.clone(),
frame_requester: tui.frame_requester(),
app_event_tx: self.app_event_tx.clone(),
initial_prompt: None,
initial_images: Vec::new(),
enhanced_keys_supported: self.enhanced_keys_supported,
auth_manager: self.auth_manager.clone(),
models_manager: self.server.get_models_manager(),
feedback: self.feedback.clone(),
is_first_run: false,
};
self.chat_widget =
crate::chatwidget::ChatWidget::new_from_existing(init, thread, session_configured);
// Trim transcript up to the selected user message and re-render it.
self.trim_transcript_for_backtrack(nth_user_message);
self.apply_backtrack_rollback(selection);
self.render_transcript_once(tui);
if !prefill.is_empty() {
self.chat_widget.set_composer_text(prefill.to_string());
}
tui.frame_requester().schedule_frame();
}
fn backtrack_selection(&self, nth_user_message: usize) -> Option<BacktrackSelection> {
let base_id = self.backtrack.base_id?;
if self.chat_widget.conversation_id() != Some(base_id) {
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();
Some(BacktrackSelection {
nth_user_message,
prefill,
})
}
/// Trim transcript_cells to preserve only content up to the selected user message.
fn trim_transcript_for_backtrack(&mut self, nth_user_message: usize) {

View file

@ -1,7 +1,6 @@
use std::path::PathBuf;
use codex_common::approval_presets::ApprovalPreset;
use codex_core::protocol::ConversationPathResponseEvent;
use codex_core::protocol::Event;
use codex_core::protocol::RateLimitSnapshot;
use codex_file_search::FileMatch;
@ -180,9 +179,6 @@ pub(crate) enum AppEvent {
/// Re-open the approval presets popup.
OpenApprovalsPopup,
/// Forwarded conversation history snapshot from the current conversation.
ConversationHistory(ConversationPathResponseEvent),
/// Open the branch picker option from the review popup.
OpenReviewBranchPicker(PathBuf),