tui: preserve selected resume target path

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-15 23:32:52 -07:00
co-authored by Codex
parent 648739854e
commit ef2b09c61a
5 changed files with 81 additions and 10 deletions
+4 -8
View File
@@ -2523,14 +2523,7 @@ impl App {
AppEvent::OpenResumePicker => {
match crate::resume_picker::run_resume_picker(tui, &self.config, false).await? {
SessionSelection::Resume(target_session) => {
self.chat_widget.handle_serialized_slash_command(
format!(
"/{} {}",
crate::slash_command::SlashCommand::Resume.command(),
target_session.thread_id
)
.into(),
);
self.chat_widget.handle_resume_selection(target_session);
self.refresh_status_line();
}
SessionSelection::Exit
@@ -2544,6 +2537,9 @@ impl App {
AppEvent::ResumeSession(thread_id) => {
self.resume_session_by_thread_id(tui, thread_id).await?;
}
AppEvent::ResumeSessionTarget(target_session) => {
self.resume_session_target(tui, target_session).await?;
}
AppEvent::ForkCurrentSession => {
self.session_telemetry.counter(
"codex.thread.fork",
+3
View File
@@ -101,6 +101,9 @@ pub(crate) enum AppEvent {
/// Resume a saved session by thread id.
ResumeSession(ThreadId),
/// Resume a saved session using the exact picker-selected rollout target.
ResumeSessionTarget(crate::resume_picker::SessionTarget),
/// Fork the current session into a new thread.
ForkCurrentSession,
+45 -1
View File
@@ -268,6 +268,7 @@ use crate::render::renderable::FlexRenderable;
use crate::render::renderable::Renderable;
use crate::render::renderable::RenderableExt;
use crate::render::renderable::RenderableItem;
use crate::resume_picker::SessionTarget;
use crate::slash_command::SlashCommand;
use crate::status::RateLimitSnapshotDisplay;
use crate::status_indicator_widget::STATUS_DETAILS_DEFAULT_MAX_LINES;
@@ -735,6 +736,11 @@ pub(crate) struct ChatWidget {
// drafts and are replayed through the slash-command evaluator instead of being submitted
// directly as user turns.
queued_user_messages: VecDeque<UserMessage>,
// Exact rollout targets picked from the resume picker for queued `/resume <thread-id>` drafts.
//
// The serialized draft stays human-readable, but replay consumes these targets in queue order
// so the selected rollout path is preserved instead of being re-resolved by thread id later.
queued_resume_targets: VecDeque<SessionTarget>,
// Steers already submitted to core but not yet committed into history.
//
// The bottom pane shows these above queued drafts until core records the
@@ -893,6 +899,7 @@ pub(crate) struct ThreadInputState {
composer: Option<ThreadComposerState>,
pending_steers: VecDeque<UserMessage>,
queued_user_messages: VecDeque<UserMessage>,
queued_resume_targets: VecDeque<SessionTarget>,
current_collaboration_mode: CollaborationMode,
active_collaboration_mask: Option<CollaborationModeMask>,
agent_turn_running: bool,
@@ -2268,6 +2275,7 @@ impl ChatWidget {
.map(|steer| steer.user_message)
.collect();
to_merge.extend(self.queued_user_messages.drain(..));
self.queued_resume_targets.clear();
if has_existing_message {
to_merge.push(existing_message);
}
@@ -2310,6 +2318,7 @@ impl ChatWidget {
.map(|pending| pending.user_message.clone())
.collect(),
queued_user_messages: self.queued_user_messages.clone(),
queued_resume_targets: self.queued_resume_targets.clone(),
current_collaboration_mode: self.current_collaboration_mode.clone(),
active_collaboration_mask: self.active_collaboration_mask.clone(),
agent_turn_running: self.agent_turn_running,
@@ -2352,6 +2361,7 @@ impl ChatWidget {
self.queued_user_messages = input_state.pending_steers;
self.queued_user_messages
.extend(input_state.queued_user_messages);
self.queued_resume_targets = input_state.queued_resume_targets;
} else {
self.agent_turn_running = false;
self.pending_steers.clear();
@@ -2364,6 +2374,7 @@ impl ChatWidget {
);
self.bottom_pane.set_composer_pending_pastes(Vec::new());
self.queued_user_messages.clear();
self.queued_resume_targets.clear();
}
self.turn_sleep_inhibitor
.set_turn_running(self.agent_turn_running);
@@ -3645,6 +3656,7 @@ impl ChatWidget {
thread_name: None,
forked_from: None,
queued_user_messages: VecDeque::new(),
queued_resume_targets: VecDeque::new(),
pending_steers: VecDeque::new(),
submit_pending_steers_after_interrupt: false,
queued_message_edit_binding,
@@ -3836,6 +3848,7 @@ impl ChatWidget {
plan_delta_buffer: String::new(),
plan_item_active: false,
queued_user_messages: VecDeque::new(),
queued_resume_targets: VecDeque::new(),
pending_steers: VecDeque::new(),
submit_pending_steers_after_interrupt: false,
queued_message_edit_binding,
@@ -4011,6 +4024,7 @@ impl ChatWidget {
thread_name: None,
forked_from: None,
queued_user_messages: VecDeque::new(),
queued_resume_targets: VecDeque::new(),
pending_steers: VecDeque::new(),
submit_pending_steers_after_interrupt: false,
queued_message_edit_binding,
@@ -4145,6 +4159,12 @@ impl ChatWidget {
&& !self.queued_user_messages.is_empty()
{
if let Some(queued_message) = self.queued_user_messages.pop_back() {
if self
.parse_builtin_slash_command(&queued_message.text)
.is_some_and(|(cmd, _, _)| cmd == SlashCommand::Resume)
{
self.queued_resume_targets.pop_back();
}
self.restore_user_message_to_composer(queued_message);
self.refresh_pending_input_preview();
self.request_redraw();
@@ -5324,7 +5344,20 @@ impl ChatWidget {
}
match ThreadId::from_string(&args[0]) {
Ok(thread_id) => {
self.app_event_tx.send(AppEvent::ResumeSession(thread_id));
if self
.queued_resume_targets
.front()
.is_some_and(|target| target.thread_id == thread_id)
{
if let Some(target_session) = self.queued_resume_targets.pop_front() {
self.app_event_tx
.send(AppEvent::ResumeSessionTarget(target_session));
} else {
self.app_event_tx.send(AppEvent::ResumeSession(thread_id));
}
} else {
self.app_event_tx.send(AppEvent::ResumeSession(thread_id));
}
QueueReplayControl::Stop
}
Err(_) => self.restore_invalid_inline_slash_command(
@@ -5712,6 +5745,17 @@ impl ChatWidget {
let _ = self.execute_serialized_slash_command(draft);
}
pub(crate) fn handle_resume_selection(&mut self, target_session: SessionTarget) {
self.queued_resume_targets.push_back(target_session.clone());
self.handle_serialized_slash_command(
SlashCommandInvocation::with_args(
SlashCommand::Resume,
[target_session.thread_id.to_string()],
)
.into_user_message(),
);
}
fn submit_plan_user_message(&mut self, user_message: UserMessage) {
if self.is_session_configured() {
self.reasoning_buffer.clear();
+28
View File
@@ -1902,6 +1902,7 @@ async fn make_chatwidget_manual(
show_welcome_banner: true,
startup_tooltip_override: None,
queued_user_messages: VecDeque::new(),
queued_resume_targets: VecDeque::new(),
pending_steers: VecDeque::new(),
submit_pending_steers_after_interrupt: false,
queued_message_edit_binding: crate::key_hint::alt(KeyCode::Up),
@@ -3681,6 +3682,7 @@ async fn restore_thread_input_state_syncs_sleep_inhibitor_state() {
composer: None,
pending_steers: VecDeque::new(),
queued_user_messages: VecDeque::new(),
queued_resume_targets: VecDeque::new(),
current_collaboration_mode: chat.current_collaboration_mode.clone(),
active_collaboration_mask: chat.active_collaboration_mask.clone(),
agent_turn_running: true,
@@ -5710,6 +5712,32 @@ async fn queued_plan_replay_stops_after_submitting_user_turn() {
assert_no_submit_op(&mut op_rx);
}
#[tokio::test]
async fn queued_resume_picker_selection_replays_exact_targets_in_order() {
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(None).await;
chat.bottom_pane.set_task_running(true);
let thread_id = ThreadId::new();
let first = crate::resume_picker::SessionTarget {
path: PathBuf::from("/tmp/first.rollout"),
thread_id,
};
let second = crate::resume_picker::SessionTarget {
path: PathBuf::from("/tmp/second.rollout"),
thread_id,
};
chat.handle_resume_selection(first.clone());
chat.handle_resume_selection(second.clone());
chat.bottom_pane.set_task_running(false);
chat.drain_queued_inputs_until_blocked();
assert_matches!(rx.try_recv(), Ok(AppEvent::ResumeSessionTarget(target)) if target == first);
chat.drain_queued_inputs_until_blocked();
assert_matches!(rx.try_recv(), Ok(AppEvent::ResumeSessionTarget(target)) if target == second);
}
#[tokio::test]
async fn collaboration_modes_defaults_to_code_on_startup() {
let codex_home = tempdir().expect("tempdir");
+1 -1
View File
@@ -40,7 +40,7 @@ use unicode_width::UnicodeWidthStr;
const PAGE_SIZE: usize = 25;
const LOAD_NEAR_THRESHOLD: usize = 5;
#[derive(Debug, Clone)]
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SessionTarget {
pub path: PathBuf,
pub thread_id: ThreadId,