tui: preserve queued drafts across session switches

Co-authored-by: Codex <noreply@openai.com>
This commit is contained in:
Charles Cunningham
2026-03-15 23:36:06 -07:00
co-authored by Codex
parent e0627c8f37
commit 6d0be7f9d8
+65
View File
@@ -888,6 +888,7 @@ impl App {
.await
{
Ok(resumed) => {
let input_state = self.chat_widget.capture_thread_input_state();
self.shutdown_current_thread().await;
self.config = resume_config;
tui.set_notification_method(self.config.tui_notification_method);
@@ -905,6 +906,7 @@ impl App {
}
self.chat_widget.add_plain_history_lines(lines);
}
self.restore_input_state_after_thread_switch(input_state);
}
Err(err) => {
let path_display = target_session.path.display();
@@ -1888,6 +1890,11 @@ impl App {
self.sync_active_agent_label();
}
fn restore_input_state_after_thread_switch(&mut self, input_state: Option<ThreadInputState>) {
self.chat_widget.restore_thread_input_state(input_state);
self.chat_widget.drain_queued_inputs_until_blocked();
}
async fn start_fresh_session_with_summary_hint(&mut self, tui: &mut tui::Tui) {
// Start a fresh in-memory session while preserving resumability via persisted rollout
// history.
@@ -1900,6 +1907,7 @@ impl App {
self.chat_widget.thread_id(),
self.chat_widget.thread_name(),
);
let input_state = self.chat_widget.capture_thread_input_state();
self.shutdown_current_thread().await;
let report = self
.server
@@ -1939,6 +1947,7 @@ impl App {
}
self.chat_widget.add_plain_history_lines(lines);
}
self.restore_input_state_after_thread_switch(input_state);
tui.frame_requester().schedule_frame();
}
@@ -2569,6 +2578,7 @@ impl App {
.await
{
Ok(forked) => {
let input_state = self.chat_widget.capture_thread_input_state();
self.shutdown_current_thread().await;
let init = self.chatwidget_init_for_forked_or_resumed_thread(
tui,
@@ -2592,6 +2602,7 @@ impl App {
}
self.chat_widget.add_plain_history_lines(lines);
}
self.restore_input_state_after_thread_switch(input_state);
}
Err(err) => {
let path_display = path.display();
@@ -4744,6 +4755,60 @@ mod tests {
}
}
#[tokio::test]
async fn thread_switch_restores_and_drains_queued_follow_up() {
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;
let thread_id = ThreadId::new();
let session_configured = Event {
id: "session-configured".to_string(),
msg: EventMsg::SessionConfigured(SessionConfiguredEvent {
session_id: thread_id,
forked_from_id: None,
thread_name: None,
model: "gpt-test".to_string(),
model_provider_id: "test-provider".to_string(),
service_tier: None,
approval_policy: AskForApproval::Never,
approvals_reviewer: ApprovalsReviewer::User,
sandbox_policy: SandboxPolicy::new_read_only_policy(),
cwd: PathBuf::from("/tmp/project"),
reasoning_effort: None,
history_log_id: 0,
history_entry_count: 0,
initial_messages: None,
network_proxy: None,
rollout_path: Some(PathBuf::new()),
}),
};
app.chat_widget
.apply_external_edit("queued follow-up".to_string());
app.chat_widget
.handle_key_event(KeyEvent::new(KeyCode::Enter, KeyModifiers::NONE));
let input_state = app
.chat_widget
.capture_thread_input_state()
.expect("expected queued follow-up state");
let (chat_widget, _app_event_tx, _rx, mut new_op_rx) =
make_chatwidget_manual_with_sender().await;
app.chat_widget = chat_widget;
app.chat_widget.handle_codex_event(session_configured);
while new_op_rx.try_recv().is_ok() {}
app.restore_input_state_after_thread_switch(Some(input_state));
match next_user_turn_op(&mut new_op_rx) {
Op::UserTurn { items, .. } => assert_eq!(
items,
vec![UserInput::Text {
text: "queued follow-up".to_string(),
text_elements: Vec::new(),
}]
),
other => panic!("expected queued follow-up submission, got {other:?}"),
}
}
#[tokio::test]
async fn replay_only_thread_keeps_restored_queue_visible() {
let (mut app, _app_event_rx, _op_rx) = make_test_app_with_channels().await;