mirror of
https://github.com/openai/codex.git
synced 2026-11-05 08:28:29 +00:00
Add /archive slash command (#25027)
## Why TUI users can archive saved sessions from other surfaces, but there is no in-session command for archiving the active session. Since archiving the active session also exits the TUI, the command should ask for explicit confirmation instead of firing immediately. I'm also working on [a companion PR](https://github.com/openai/codex/pull/25021) that adds `codex archive` and `codex unarchive` top-level CLI commands. ## What changed - Adds a new `/archive` slash command described as `archive this session and exit`. - Shows a confirmation dialog with `No, don't archive` selected first and `Yes, archive and exit` as the explicit action. - On confirmation, calls the existing `thread/archive` app-server RPC for the active main session and exits after success. - Keeps `/archive` disabled while a task is running and unavailable in side conversations. ## Verification Added focused TUI coverage for the `/archive` confirmation flow, disabled-while-task-running behavior, and the `/ar` slash-command popup snapshot.
This commit is contained in:
1 parent
1333f4a689
commit
36cd36626d
9 files changed
+168
No files matched your search
@@ -164,6 +164,35 @@ impl ChatWidget {
|
||||
SlashCommand::New => {
|
||||
self.app_event_tx.send(AppEvent::NewSession);
|
||||
}
|
||||
SlashCommand::Archive => {
|
||||
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 and exit Codex"
|
||||
.to_string(),
|
||||
),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
name: "No, don't archive".to_string(),
|
||||
description: Some("Return to the current session".to_string()),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Yes, archive and exit".to_string(),
|
||||
description: Some("Archive this session now".to_string()),
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::ArchiveCurrentThread);
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
});
|
||||
self.request_redraw();
|
||||
}
|
||||
SlashCommand::Clear => {
|
||||
self.app_event_tx.send(AppEvent::ClearUi);
|
||||
}
|
||||
@@ -949,6 +978,7 @@ impl ChatWidget {
|
||||
| SlashCommand::TestApproval => QueueDrain::Continue,
|
||||
SlashCommand::Feedback
|
||||
| SlashCommand::New
|
||||
| SlashCommand::Archive
|
||||
| SlashCommand::Clear
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Fork
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/slash_commands.rs
|
||||
expression: popup
|
||||
---
|
||||
Archive this session?
|
||||
Are you sure? This will archive the current session and exit Codex
|
||||
|
||||
› 1. No, don't archive Return to the current session
|
||||
2. Yes, archive and exit Archive this session now
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -1727,6 +1727,27 @@ async fn slash_clear_is_disabled_while_task_running() {
|
||||
assert!(rx.try_recv().is_err(), "expected no follow-up events");
|
||||
}
|
||||
|
||||
#[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;
|
||||
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");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_memory_drop_reports_stubbed_feature() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
@@ -1846,6 +1867,24 @@ async fn slash_resume_opens_picker() {
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::OpenResumePicker));
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Archive);
|
||||
|
||||
assert!(chat.bottom_pane.has_active_view());
|
||||
assert_matches!(rx.try_recv(), Err(TryRecvError::Empty));
|
||||
|
||||
let popup = render_bottom_popup(&chat, /*width*/ 80);
|
||||
assert_chatwidget_snapshot!("slash_archive_confirmation_popup", popup);
|
||||
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Down));
|
||||
chat.handle_key_event(KeyEvent::from(KeyCode::Enter));
|
||||
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::ArchiveCurrentThread));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_resume_with_arg_requests_named_session() {
|
||||
let (mut chat, mut rx, mut op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
Reference in new issue
Block a user