tui: serialize exact resume picker targets

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 7bcff58ea4
commit 295f7fe976
8 changed files with 37 additions and 54 deletions
+3 -1
View File
@@ -2523,7 +2523,9 @@ impl App {
AppEvent::OpenResumePicker => {
match crate::resume_picker::run_resume_picker(tui, &self.config, false).await? {
SessionSelection::Resume(target_session) => {
self.chat_widget.handle_resume_selection(target_session);
self.chat_widget.handle_serialized_slash_command(
ChatWidget::resume_selection_draft(&target_session),
);
self.refresh_status_line();
}
SessionSelection::Exit
+25 -46
View File
@@ -268,7 +268,6 @@ 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;
@@ -736,11 +735,6 @@ 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
@@ -899,7 +893,6 @@ 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,
@@ -2275,7 +2268,6 @@ 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);
}
@@ -2318,7 +2310,6 @@ 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,
@@ -2361,7 +2352,6 @@ 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();
@@ -2374,7 +2364,6 @@ 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);
@@ -3656,7 +3645,6 @@ 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,
@@ -3848,7 +3836,6 @@ 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,
@@ -4024,7 +4011,6 @@ 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,
@@ -4159,12 +4145,6 @@ 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 +5304,7 @@ impl ChatWidget {
SlashCommand::Resume => {
let args = match SlashCommandInvocation::parse_args(
&args_message.text,
"Usage: /resume <thread-id>",
"Usage: /resume <thread-id> [--path <rollout-path>]",
) {
Ok(args) => args,
Err(message) => {
@@ -5335,26 +5315,22 @@ impl ChatWidget {
);
}
};
if args.len() != 1 {
if !(args.len() == 1 || (args.len() == 3 && args[1] == "--path")) {
return self.restore_invalid_inline_slash_command(
cmd,
args_message,
"Usage: /resume <thread-id>".to_string(),
"Usage: /resume <thread-id> [--path <rollout-path>]".to_string(),
);
}
match ThreadId::from_string(&args[0]) {
Ok(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));
}
if let Some(path) = args.get(2) {
self.app_event_tx.send(AppEvent::ResumeSessionTarget(
crate::resume_picker::SessionTarget {
path: PathBuf::from(path),
thread_id,
},
));
} else {
self.app_event_tx.send(AppEvent::ResumeSession(thread_id));
}
@@ -5363,7 +5339,7 @@ impl ChatWidget {
Err(_) => self.restore_invalid_inline_slash_command(
cmd,
args_message,
"Usage: /resume <thread-id>".to_string(),
"Usage: /resume <thread-id> [--path <rollout-path>]".to_string(),
),
}
}
@@ -5729,6 +5705,20 @@ impl ChatWidget {
}
}
pub(crate) fn resume_selection_draft(
target_session: &crate::resume_picker::SessionTarget,
) -> UserMessage {
SlashCommandInvocation::with_args(
SlashCommand::Resume,
[
target_session.thread_id.to_string(),
"--path".to_string(),
target_session.path.display().to_string(),
],
)
.into_user_message()
}
pub(crate) fn handle_serialized_slash_command(&mut self, draft: UserMessage) {
let Some((cmd, _, _)) = self.parse_builtin_slash_command(&draft.text) else {
if self.reject_unavailable_builtin_slash_command(&draft) {
@@ -5745,17 +5735,6 @@ 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();
@@ -83,6 +83,7 @@ expression: popup
Usage:
/resume
/resume <thread-id>
/resume <thread-id> --path <rollout-path>
/fork
fork the current chat
@@ -240,4 +241,4 @@ expression: popup
/debug-m-drop
↑/↓ scroll | [ctrl + p / ctrl + n] page | / search | esc close 1-232/239
↑/↓ scroll | [ctrl + p / ctrl + n] page | / search | esc close 1-233/240
@@ -88,6 +88,7 @@ expression: popup
Usage:
/resume
/resume <thread-id>
/resume <thread-id> --path <rollout-path>
/fork
fork the current chat
@@ -83,6 +83,7 @@ expression: searching
Usage:
/resume
/resume <thread-id>
/resume <thread-id> --path <rollout-path>
/fork
fork the current chat
@@ -240,4 +241,4 @@ expression: searching
/debug-m-drop
Search: /maintainers | enter apply | esc cancel 1 match | 1-232/239
Search: /maintainers | enter apply | esc cancel 1 match | 1-233/240
@@ -88,6 +88,7 @@ expression: searching
Usage:
/resume
/resume <thread-id>
/resume <thread-id> --path <rollout-path>
/fork
fork the current chat
+2 -4
View File
@@ -1902,7 +1902,6 @@ 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),
@@ -3682,7 +3681,6 @@ 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,
@@ -5727,8 +5725,8 @@ async fn queued_resume_picker_selection_replays_exact_targets_in_order() {
thread_id,
};
chat.handle_resume_selection(first.clone());
chat.handle_resume_selection(second.clone());
chat.handle_serialized_slash_command(ChatWidget::resume_selection_draft(&first));
chat.handle_serialized_slash_command(ChatWidget::resume_selection_draft(&second));
chat.bottom_pane.set_task_running(false);
chat.drain_queued_inputs_until_blocked();
+1 -1
View File
@@ -150,7 +150,7 @@ impl SlashCommand {
],
SlashCommand::Rename => &["", "<title...>"],
SlashCommand::New => &[""],
SlashCommand::Resume => &["", "<thread-id>"],
SlashCommand::Resume => &["", "<thread-id>", "<thread-id> --path <rollout-path>"],
SlashCommand::Fork => &[""],
SlashCommand::Init => &[""],
SlashCommand::Compact => &[""],