mirror of
https://github.com/openai/codex.git
synced 2026-11-02 19:43:24 +00:00
tui: preserve selected resume target path
Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
co-authored by
Codex
parent
648739854e
commit
ef2b09c61a
@@ -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",
|
||||
|
||||
@@ -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,
|
||||
|
||||
|
||||
@@ -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();
|
||||
|
||||
@@ -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");
|
||||
|
||||
@@ -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,
|
||||
|
||||
Reference in New Issue
Block a user