From dde5f8c5ae9b644c6882b3a48581ac317255d597 Mon Sep 17 00:00:00 2001 From: Benjamin Carlsson Date: Sun, 4 Oct 2026 02:23:02 +0000 Subject: [PATCH] Allow `/archive` while a turn is running (#50764) ## Why `/archive` was disabled during a running turn, preventing users from archiving the current session until the turn ended. ## What changed Enable `/archive` during a running turn and warn in the confirmation dialog that archiving will stop the current turn. Cancelling the dialog leaves the turn running. ## Testing Extend confirmation tests to cover both idle and running sessions, add a snapshot of the running-turn warning, and verify that cancellation leaves the task running without emitting archive events or operations. GitOrigin-RevId: c7c530aa671b5f04e5dc2a637e3d27a81d98d8bf --- codex-rs/tui/src/chatwidget/slash_dispatch.rs | 7 ++- ...sh_archive_running_confirmation_popup.snap | 13 +++++ .../src/chatwidget/tests/slash_commands.rs | 49 ++++++++++--------- codex-rs/tui/src/slash_command.rs | 2 +- 4 files changed, 46 insertions(+), 25 deletions(-) create mode 100644 codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_running_confirmation_popup.snap diff --git a/codex-rs/tui/src/chatwidget/slash_dispatch.rs b/codex-rs/tui/src/chatwidget/slash_dispatch.rs index 9a39bfa473..967c1aa087 100644 --- a/codex-rs/tui/src/chatwidget/slash_dispatch.rs +++ b/codex-rs/tui/src/chatwidget/slash_dispatch.rs @@ -238,7 +238,12 @@ impl ChatWidget { self.bottom_pane.show_selection_view(SelectionViewParams { title: Some("Archive this session?".to_string()), subtitle: Some( - "Are you sure? This will archive the current session".to_string(), + if self.bottom_pane.is_task_running() { + "This will stop the current turn and archive the session." + } else { + "Are you sure? This will archive the current session" + } + .to_string(), ), footer_hint: Some(standard_popup_hint_line()), items: vec![ diff --git a/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_running_confirmation_popup.snap b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_running_confirmation_popup.snap new file mode 100644 index 0000000000..0642317545 --- /dev/null +++ b/codex-rs/tui/src/chatwidget/snapshots/codex_tui__chatwidget__tests__slash_archive_running_confirmation_popup.snap @@ -0,0 +1,13 @@ +--- +source: tui/src/chatwidget/tests/slash_commands.rs +assertion_line: 2937 +expression: popup +--- +• Wo Archive this session? + This will stop the current turn and archive the session. + +› As + › 1. No, don't archive Return to the current session + GP 2. Yes, archive Archive this session now + + enter select · esc back diff --git a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs index bc1ba4e4fe..2fcb0b9218 100644 --- a/codex-rs/tui/src/chatwidget/tests/slash_commands.rs +++ b/codex-rs/tui/src/chatwidget/tests/slash_commands.rs @@ -2755,24 +2755,19 @@ async fn slash_clear_is_disabled_while_task_running() { } #[tokio::test] -async fn slash_archive_is_disabled_while_task_running() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; +async fn slash_archive_cancellation_keeps_task_running() { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; chat.bottom_pane.set_task_running(/*running*/ true); chat.dispatch_command(SlashCommand::Archive); - let event = rx.try_recv().expect("expected disabled command error"); - match event { - AppEvent::InsertHistoryCell(cell) => { - let rendered = lines_to_single_string(&cell.display_lines(/*width*/ 80)); - assert!( - rendered.contains("'/archive' is disabled while a task is in progress."), - "expected /archive task-running error, got {rendered:?}" - ); - } - other => panic!("expected InsertHistoryCell error, got {other:?}"), - } - assert!(rx.try_recv().is_err(), "expected no follow-up events"); + assert!(chat.bottom_pane.has_active_view()); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + + assert!(!chat.bottom_pane.has_active_view()); + assert!(chat.bottom_pane.is_task_running()); + assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); } #[tokio::test] @@ -2927,20 +2922,28 @@ async fn slash_import_opens_claude_code_import_picker() { #[tokio::test] async fn slash_archive_confirmation_requests_current_thread_archive() { - let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + for running in [false, true] { + let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await; + chat.bottom_pane.set_task_running(running); - chat.dispatch_command(SlashCommand::Archive); + chat.dispatch_command(SlashCommand::Archive); - assert!(chat.bottom_pane.has_active_view()); - assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); + assert!(chat.bottom_pane.has_active_view()); + assert_matches!(rx.try_recv(), Err(TryRecvError::Empty)); + assert_matches!(op_rx.try_recv(), Err(TryRecvError::Empty)); - let popup = render_bottom_popup(&chat, /*width*/ 80); - assert_chatwidget_snapshot!("slash_archive_confirmation_popup", popup); + let popup = render_bottom_popup(&chat, /*width*/ 80); + if running { + assert_chatwidget_snapshot!("slash_archive_running_confirmation_popup", popup); + } else { + assert_chatwidget_snapshot!("slash_archive_confirmation_popup", popup); + } - chat.handle_key_event(KeyEvent::from(KeyCode::Down)); - chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); + chat.handle_key_event(KeyEvent::from(KeyCode::Down)); + chat.handle_key_event(KeyEvent::from(KeyCode::Enter)); - assert_matches!(rx.try_recv(), Ok(AppEvent::ArchiveCurrentThread)); + assert_matches!(rx.try_recv(), Ok(AppEvent::ArchiveCurrentThread)); + } } #[tokio::test] diff --git a/codex-rs/tui/src/slash_command.rs b/codex-rs/tui/src/slash_command.rs index 945041b644..d9ba053eb5 100644 --- a/codex-rs/tui/src/slash_command.rs +++ b/codex-rs/tui/src/slash_command.rs @@ -241,7 +241,6 @@ impl SlashCommand { pub fn available_during_task(self) -> bool { match self { SlashCommand::New - | SlashCommand::Archive | SlashCommand::Delete | SlashCommand::Fork | SlashCommand::Worktree @@ -264,6 +263,7 @@ impl SlashCommand { | SlashCommand::MemoryDrop | SlashCommand::MemoryUpdate => false, SlashCommand::Diff + | SlashCommand::Archive | SlashCommand::Resume | SlashCommand::Model | SlashCommand::Daybreak