mirror of
https://github.com/pchuan98/codex.git
synced 2026-07-01 00:31:56 +08:00
Add session delete commands in CLI and TUI (#27476)
## Summary The app server exposes `thread/delete`, but users cannot invoke it from the CLI or TUI. Because deletion is irreversible, the user-facing commands need deliberate confirmation and safer handling of name-based targets. - Add `codex delete <SESSION>` with interactive confirmation, restricting `--force` to UUID targets. - Resolve exact names across active and archived sessions, including renamed sessions, and validate prompted UUID targets before confirmation. - Add a `/delete` command with a confirmation popup that warns the current session and its subagent threads will be permanently deleted. ## Manual testing - Deleted by UUID with `--force` and verified the rollout, session-index entry, and database row were removed. - Exercised name-based confirmation for both cancellation and affirmative deletion; cancellation preserved the session and confirmation removed it. - Verified deletion refuses to proceed without `--force`, while `--force` rejects names, including duplicate names. - Verified duplicate-name confirmation displays the concrete UUID selected. - Deleted an archived session by name. - Verified an already-missing UUID fails before displaying a confirmation prompt. - Exercised `/delete` in the TUI: the popup defaults to No, cancellation preserves the session, and confirmation deletes the session and exits. - Verified that `codex delete` works for both archived and non-archived sessions.
This commit is contained in:
committed by
GitHub
Unverified
parent
36fc79c6f4
commit
9d87b771ce
@@ -180,6 +180,9 @@ enum Subcommand {
|
||||
/// Archive a saved session by id or session name.
|
||||
Archive(SessionArchiveCommand),
|
||||
|
||||
/// Permanently delete a saved session by id or session name.
|
||||
Delete(DeleteCommand),
|
||||
|
||||
/// Unarchive a saved session by id or session name.
|
||||
Unarchive(SessionArchiveCommand),
|
||||
|
||||
@@ -354,6 +357,16 @@ struct SessionArchiveConfigOverrides {
|
||||
config_overrides: CliConfigOverrides,
|
||||
}
|
||||
|
||||
#[derive(Debug, Args)]
|
||||
struct DeleteCommand {
|
||||
#[clap(flatten)]
|
||||
session: SessionArchiveCommand,
|
||||
|
||||
/// Delete without prompting. SESSION must be a UUID.
|
||||
#[arg(long, default_value_t = false)]
|
||||
force: bool,
|
||||
}
|
||||
|
||||
#[derive(Debug, Parser)]
|
||||
struct ForkCommand {
|
||||
/// Conversation/session id (UUID). When provided, forks this session.
|
||||
@@ -829,6 +842,17 @@ async fn run_session_archive_cli_command(
|
||||
.map_err(|err| anyhow::anyhow!("{err}"))
|
||||
}
|
||||
|
||||
fn delete_action(target: &str, force: bool) -> anyhow::Result<codex_tui::SessionArchiveAction> {
|
||||
if force && codex_protocol::ThreadId::from_string(target).is_err() {
|
||||
anyhow::bail!("--force requires a session UUID; names must be confirmed interactively");
|
||||
}
|
||||
let confirmation = match force {
|
||||
true => codex_tui::DeleteConfirmation::Skip,
|
||||
false => codex_tui::DeleteConfirmation::Prompt,
|
||||
};
|
||||
Ok(codex_tui::SessionArchiveAction::Delete(confirmation))
|
||||
}
|
||||
|
||||
async fn run_debug_app_server_command(cmd: DebugAppServerCommand) -> anyhow::Result<()> {
|
||||
match cmd.subcommand {
|
||||
DebugAppServerSubcommand::SendMessageV2(cmd) => {
|
||||
@@ -1233,6 +1257,20 @@ async fn cli_main(arg0_paths: Arg0DispatchPaths) -> anyhow::Result<()> {
|
||||
.await?;
|
||||
println!("{output}");
|
||||
}
|
||||
Some(Subcommand::Delete(DeleteCommand { session, force })) => {
|
||||
let action = delete_action(&session.target, force)?;
|
||||
let output = run_session_archive_cli_command(
|
||||
action,
|
||||
session,
|
||||
interactive,
|
||||
root_config_overrides.clone(),
|
||||
root_remote.clone(),
|
||||
root_remote_auth_token_env.clone(),
|
||||
arg0_paths.clone(),
|
||||
)
|
||||
.await?;
|
||||
println!("{output}");
|
||||
}
|
||||
Some(Subcommand::Unarchive(cmd)) => {
|
||||
let output = run_session_archive_cli_command(
|
||||
codex_tui::SessionArchiveAction::Unarchive,
|
||||
@@ -1597,6 +1635,7 @@ fn profile_v2_for_subcommand<'a>(
|
||||
| Subcommand::Review(_)
|
||||
| Subcommand::Resume(_)
|
||||
| Subcommand::Archive(_)
|
||||
| Subcommand::Delete(_)
|
||||
| Subcommand::Unarchive(_)
|
||||
| Subcommand::Fork(_)
|
||||
| Subcommand::Mcp(_)
|
||||
@@ -1605,7 +1644,7 @@ fn profile_v2_for_subcommand<'a>(
|
||||
subcommand: DebugSubcommand::PromptInput(_),
|
||||
}) => Ok(Some(profile_v2)),
|
||||
_ => anyhow::bail!(
|
||||
"--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex archive`, `codex unarchive`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`."
|
||||
"--profile only applies to runtime commands and `codex mcp`: `codex`, `codex exec`, `codex review`, `codex resume`, `codex archive`, `codex delete`, `codex unarchive`, `codex fork`, `codex mcp`, `codex sandbox`, and `codex debug prompt-input`."
|
||||
),
|
||||
}
|
||||
}
|
||||
@@ -2026,6 +2065,7 @@ fn unsupported_subcommand_name_for_strict_config(
|
||||
| Some(Subcommand::ExecServer(_))
|
||||
| Some(Subcommand::Resume(_))
|
||||
| Some(Subcommand::Archive(_))
|
||||
| Some(Subcommand::Delete(_))
|
||||
| Some(Subcommand::Unarchive(_))
|
||||
| Some(Subcommand::Fork(_))
|
||||
| Some(Subcommand::Doctor(_)) => None,
|
||||
@@ -2868,6 +2908,17 @@ mod tests {
|
||||
assert!(interactive.bypass_hook_trust);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn delete_force_requires_uuid() {
|
||||
assert!(delete_action("123e4567-e89b-12d3-a456-426614174000", true).is_ok());
|
||||
|
||||
let err = delete_action("my-thread", true).expect_err("name should require prompt");
|
||||
assert_eq!(
|
||||
err.to_string(),
|
||||
"--force requires a session UUID; names must be confirmed interactively"
|
||||
);
|
||||
}
|
||||
|
||||
#[cfg(any(target_os = "macos", target_os = "linux", target_os = "windows"))]
|
||||
#[test]
|
||||
fn sandbox_parses_permissions_profile() {
|
||||
|
||||
@@ -0,0 +1,17 @@
|
||||
use predicates::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn missing_session_fails_before_delete_confirmation() -> anyhow::Result<()> {
|
||||
let codex_home = tempfile::tempdir()?;
|
||||
let mut cmd = assert_cmd::Command::new(codex_utils_cargo_bin::cargo_bin("codex")?);
|
||||
cmd.env("CODEX_HOME", codex_home.path())
|
||||
.args(["delete", "123e4567-e89b-12d3-a456-426614174000"]);
|
||||
|
||||
cmd.assert()
|
||||
.failure()
|
||||
.stderr(predicate::str::contains(
|
||||
"No active or archived session found matching",
|
||||
))
|
||||
.stderr(predicate::str::contains("cannot confirm").not());
|
||||
Ok(())
|
||||
}
|
||||
@@ -153,6 +153,9 @@ impl App {
|
||||
AppEvent::ArchiveCurrentThread => {
|
||||
return Ok(self.archive_current_thread(app_server).await);
|
||||
}
|
||||
AppEvent::DeleteCurrentThread => {
|
||||
return Ok(self.delete_current_thread(app_server).await);
|
||||
}
|
||||
AppEvent::ForkCurrentSession => {
|
||||
self.session_telemetry.counter(
|
||||
"codex.thread.fork",
|
||||
@@ -2273,4 +2276,31 @@ impl App {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(super) async fn delete_current_thread(
|
||||
&mut self,
|
||||
app_server: &mut AppServerSession,
|
||||
) -> AppRunControl {
|
||||
let Some(thread_id) = self.active_thread_id.or(self.chat_widget.thread_id()) else {
|
||||
self.chat_widget
|
||||
.add_error_message("A thread must start before it can be deleted.".to_string());
|
||||
return AppRunControl::Continue;
|
||||
};
|
||||
if self.side_threads.contains_key(&thread_id) {
|
||||
self.chat_widget.add_error_message(
|
||||
"'/delete' is unavailable in side conversations. Press Ctrl+C to return to the main thread first."
|
||||
.to_string(),
|
||||
);
|
||||
return AppRunControl::Continue;
|
||||
}
|
||||
|
||||
match app_server.thread_delete(thread_id).await {
|
||||
Ok(()) => AppRunControl::Exit(ExitReason::UserRequested),
|
||||
Err(err) => {
|
||||
self.chat_widget
|
||||
.add_error_message(format!("Failed to delete current thread: {err}"));
|
||||
AppRunControl::Continue
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -230,6 +230,9 @@ pub(crate) enum AppEvent {
|
||||
/// Archive the current active main thread and exit after it succeeds.
|
||||
ArchiveCurrentThread,
|
||||
|
||||
/// Permanently delete the current active main thread and exit after it succeeds.
|
||||
DeleteCurrentThread,
|
||||
|
||||
/// Fork the current session into a new thread.
|
||||
ForkCurrentSession,
|
||||
|
||||
|
||||
@@ -53,6 +53,8 @@ use codex_app_server_protocol::ThreadBackgroundTerminalsCleanParams;
|
||||
use codex_app_server_protocol::ThreadBackgroundTerminalsCleanResponse;
|
||||
use codex_app_server_protocol::ThreadCompactStartParams;
|
||||
use codex_app_server_protocol::ThreadCompactStartResponse;
|
||||
use codex_app_server_protocol::ThreadDeleteParams;
|
||||
use codex_app_server_protocol::ThreadDeleteResponse;
|
||||
use codex_app_server_protocol::ThreadForkParams;
|
||||
use codex_app_server_protocol::ThreadForkResponse;
|
||||
use codex_app_server_protocol::ThreadGoalClearParams;
|
||||
@@ -626,6 +628,21 @@ impl AppServerSession {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_delete(&mut self, thread_id: ThreadId) -> Result<()> {
|
||||
let request_id = self.next_request_id();
|
||||
let _: ThreadDeleteResponse = self
|
||||
.client
|
||||
.request_typed(ClientRequest::ThreadDelete {
|
||||
request_id,
|
||||
params: ThreadDeleteParams {
|
||||
thread_id: thread_id.to_string(),
|
||||
},
|
||||
})
|
||||
.await
|
||||
.wrap_err("failed to delete session")?;
|
||||
Ok(())
|
||||
}
|
||||
|
||||
pub(crate) async fn thread_unarchive(&mut self, thread_id: ThreadId) -> Result<Thread> {
|
||||
let request_id = self.next_request_id();
|
||||
let response: ThreadUnarchiveResponse = self
|
||||
|
||||
@@ -193,6 +193,34 @@ impl ChatWidget {
|
||||
});
|
||||
self.request_redraw();
|
||||
}
|
||||
SlashCommand::Delete => {
|
||||
self.bottom_pane.show_selection_view(SelectionViewParams {
|
||||
title: Some("Delete this session?".to_string()),
|
||||
subtitle: Some(
|
||||
"Cannot be undone. Subagent threads will also be deleted.".to_string(),
|
||||
),
|
||||
footer_hint: Some(standard_popup_hint_line()),
|
||||
items: vec![
|
||||
SelectionItem {
|
||||
name: "No, keep this session".to_string(),
|
||||
description: Some("Return to the current session".to_string()),
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
SelectionItem {
|
||||
name: "Yes, delete and exit".to_string(),
|
||||
description: Some("Permanently delete this session now".to_string()),
|
||||
actions: vec![Box::new(|tx| {
|
||||
tx.send(AppEvent::DeleteCurrentThread);
|
||||
})],
|
||||
dismiss_on_select: true,
|
||||
..Default::default()
|
||||
},
|
||||
],
|
||||
..Default::default()
|
||||
});
|
||||
self.request_redraw();
|
||||
}
|
||||
SlashCommand::Clear => {
|
||||
self.app_event_tx.send(AppEvent::ClearUi);
|
||||
}
|
||||
@@ -986,6 +1014,7 @@ impl ChatWidget {
|
||||
SlashCommand::Feedback
|
||||
| SlashCommand::New
|
||||
| SlashCommand::Archive
|
||||
| SlashCommand::Delete
|
||||
| SlashCommand::Clear
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Fork
|
||||
|
||||
+11
@@ -0,0 +1,11 @@
|
||||
---
|
||||
source: tui/src/chatwidget/tests/slash_commands.rs
|
||||
expression: popup
|
||||
---
|
||||
Delete this session?
|
||||
Cannot be undone. Subagent threads will also be deleted.
|
||||
|
||||
› 1. No, keep this session Return to the current session
|
||||
2. Yes, delete and exit Permanently delete this session now
|
||||
|
||||
Press enter to confirm or esc to go back
|
||||
@@ -1891,6 +1891,24 @@ async fn slash_archive_confirmation_requests_current_thread_archive() {
|
||||
assert_matches!(rx.try_recv(), Ok(AppEvent::ArchiveCurrentThread));
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn slash_delete_confirmation_requests_current_thread_delete() {
|
||||
let (mut chat, mut rx, _op_rx) = make_chatwidget_manual(/*model_override*/ None).await;
|
||||
|
||||
chat.dispatch_command(SlashCommand::Delete);
|
||||
|
||||
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_delete_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::DeleteCurrentThread));
|
||||
}
|
||||
|
||||
#[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;
|
||||
|
||||
@@ -64,6 +64,7 @@ use codex_utils_oss::ensure_oss_provider_ready;
|
||||
use codex_utils_oss::get_default_model_for_oss_provider;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use cwd_prompt::CwdPromptAction;
|
||||
pub use session_archive_commands::DeleteConfirmation;
|
||||
pub use session_archive_commands::SessionArchiveAction;
|
||||
pub use session_archive_commands::SessionArchiveCommandOptions;
|
||||
pub use session_archive_commands::run_session_archive_command;
|
||||
|
||||
@@ -1,8 +1,10 @@
|
||||
//! Shared implementation for `codex archive` and `codex unarchive`.
|
||||
//! Shared implementation for `codex archive`, `codex delete`, and `codex unarchive`.
|
||||
//!
|
||||
//! The CLI commands are thin app-server clients: resolve a user-provided UUID or exact session
|
||||
//! name, then call the existing `thread/archive` or `thread/unarchive` RPC.
|
||||
//! name, then call the corresponding app-server RPC.
|
||||
|
||||
use std::io::IsTerminal;
|
||||
use std::io::Write;
|
||||
use std::sync::Arc;
|
||||
|
||||
use crate::Cli;
|
||||
@@ -26,16 +28,22 @@ use codex_protocol::ThreadId;
|
||||
use codex_utils_cli::CliConfigOverrides;
|
||||
use codex_utils_home_dir::find_codex_home;
|
||||
use codex_utils_oss::get_default_model_for_oss_provider;
|
||||
use color_eyre::eyre::ContextCompat;
|
||||
use color_eyre::eyre::Result;
|
||||
use color_eyre::eyre::WrapErr;
|
||||
use color_eyre::eyre::eyre;
|
||||
|
||||
use super::RemoteAppServerEndpoint;
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum DeleteConfirmation {
|
||||
Prompt,
|
||||
Skip,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
pub enum SessionArchiveAction {
|
||||
Archive,
|
||||
Delete(DeleteConfirmation),
|
||||
Unarchive,
|
||||
}
|
||||
|
||||
@@ -52,6 +60,7 @@ fn success_message(
|
||||
) -> String {
|
||||
let action = match action {
|
||||
SessionArchiveAction::Archive => "Archived",
|
||||
SessionArchiveAction::Delete(_) => "Deleted",
|
||||
SessionArchiveAction::Unarchive => "Unarchived",
|
||||
};
|
||||
match session_name {
|
||||
@@ -80,25 +89,30 @@ async fn run_session_archive_action_with_app_server(
|
||||
target: &str,
|
||||
) -> Result<String> {
|
||||
let resolved = resolve_session_target(app_server, action, target).await?;
|
||||
match action {
|
||||
let session_name = match action {
|
||||
SessionArchiveAction::Archive => {
|
||||
app_server.thread_archive(resolved.session_id).await?;
|
||||
Ok(success_message(
|
||||
action,
|
||||
resolved.session_id,
|
||||
resolved.session_name.as_deref(),
|
||||
))
|
||||
resolved.session_name
|
||||
}
|
||||
SessionArchiveAction::Delete(confirmation) => {
|
||||
if matches!(confirmation, DeleteConfirmation::Prompt)
|
||||
&& !confirm_session_delete(&resolved)?
|
||||
{
|
||||
return Ok("Delete cancelled.".to_string());
|
||||
}
|
||||
app_server.thread_delete(resolved.session_id).await?;
|
||||
resolved.session_name
|
||||
}
|
||||
SessionArchiveAction::Unarchive => {
|
||||
let thread = app_server.thread_unarchive(resolved.session_id).await?;
|
||||
let session_name = thread.name.or(resolved.session_name);
|
||||
Ok(success_message(
|
||||
action,
|
||||
resolved.session_id,
|
||||
session_name.as_deref(),
|
||||
))
|
||||
thread.name.or(resolved.session_name)
|
||||
}
|
||||
}
|
||||
};
|
||||
Ok(success_message(
|
||||
action,
|
||||
resolved.session_id,
|
||||
session_name.as_deref(),
|
||||
))
|
||||
}
|
||||
|
||||
async fn resolve_session_target(
|
||||
@@ -107,61 +121,83 @@ async fn resolve_session_target(
|
||||
target: &str,
|
||||
) -> Result<ResolvedSessionTarget> {
|
||||
if let Ok(session_id) = ThreadId::from_string(target) {
|
||||
if matches!(
|
||||
action,
|
||||
SessionArchiveAction::Delete(DeleteConfirmation::Prompt)
|
||||
) {
|
||||
let thread = app_server
|
||||
.thread_read(session_id, /*include_turns*/ false)
|
||||
.await
|
||||
.with_context(|| {
|
||||
format!("No active or archived session found matching '{target}'.")
|
||||
})?;
|
||||
return Ok(ResolvedSessionTarget {
|
||||
session_id,
|
||||
session_name: thread.name,
|
||||
});
|
||||
}
|
||||
return Ok(ResolvedSessionTarget {
|
||||
session_id,
|
||||
session_name: None,
|
||||
});
|
||||
}
|
||||
|
||||
let search_scope = match action {
|
||||
SessionArchiveAction::Archive => "active",
|
||||
SessionArchiveAction::Unarchive => "archived",
|
||||
let (search_scope, archived_values): (&str, &[bool]) = match action {
|
||||
SessionArchiveAction::Archive => ("active", &[false]),
|
||||
SessionArchiveAction::Delete(_) => ("active or archived", &[false, true]),
|
||||
SessionArchiveAction::Unarchive => ("archived", &[true]),
|
||||
};
|
||||
let resolved = lookup_session_by_exact_name(app_server, action, target)
|
||||
.await?
|
||||
.map(session_target_from_app_server_thread)
|
||||
.transpose()?;
|
||||
|
||||
resolved.with_context(|| format!("No {search_scope} session found matching '{target}'."))
|
||||
for &archived in archived_values {
|
||||
if let Some(thread) = lookup_session_by_exact_name(app_server, target, archived).await? {
|
||||
return session_target_from_app_server_thread(thread);
|
||||
}
|
||||
}
|
||||
Err(eyre!(
|
||||
"No {search_scope} session found matching '{target}'."
|
||||
))
|
||||
}
|
||||
|
||||
async fn lookup_session_by_exact_name(
|
||||
app_server: &mut AppServerSession,
|
||||
action: SessionArchiveAction,
|
||||
name: &str,
|
||||
archived: bool,
|
||||
) -> Result<Option<AppServerThread>> {
|
||||
let mut cursor = None;
|
||||
loop {
|
||||
let response = app_server
|
||||
.thread_list(ThreadListParams {
|
||||
cursor: cursor.clone(),
|
||||
limit: Some(100),
|
||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||
sort_direction: None,
|
||||
model_providers: None,
|
||||
source_kinds: Some(super::resume_source_kinds(
|
||||
/*include_non_interactive*/ false,
|
||||
)),
|
||||
archived: Some(matches!(action, SessionArchiveAction::Unarchive)),
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: Some(name.to_string()),
|
||||
})
|
||||
.await
|
||||
.wrap_err("failed to list sessions while resolving session name")?;
|
||||
// Search is the fast path, but some stores attach renamed titles after applying the filter.
|
||||
for search_term in [Some(name), None] {
|
||||
let mut cursor = None;
|
||||
loop {
|
||||
let response = app_server
|
||||
.thread_list(ThreadListParams {
|
||||
cursor: cursor.clone(),
|
||||
limit: Some(100),
|
||||
sort_key: Some(ThreadSortKey::UpdatedAt),
|
||||
sort_direction: None,
|
||||
model_providers: None,
|
||||
source_kinds: Some(super::resume_source_kinds(
|
||||
/*include_non_interactive*/ false,
|
||||
)),
|
||||
archived: Some(archived),
|
||||
cwd: None,
|
||||
use_state_db_only: false,
|
||||
search_term: search_term.map(str::to_string),
|
||||
})
|
||||
.await
|
||||
.wrap_err("failed to list sessions while resolving session name")?;
|
||||
|
||||
if let Some(thread) = response
|
||||
.data
|
||||
.into_iter()
|
||||
.find(|thread| thread.name.as_deref() == Some(name))
|
||||
{
|
||||
return Ok(Some(thread));
|
||||
if let Some(thread) = response
|
||||
.data
|
||||
.into_iter()
|
||||
.find(|thread| thread.name.as_deref() == Some(name))
|
||||
{
|
||||
return Ok(Some(thread));
|
||||
}
|
||||
let Some(next_cursor) = response.next_cursor else {
|
||||
break;
|
||||
};
|
||||
cursor = Some(next_cursor);
|
||||
}
|
||||
if response.next_cursor.is_none() {
|
||||
return Ok(None);
|
||||
}
|
||||
cursor = response.next_cursor;
|
||||
}
|
||||
Ok(None)
|
||||
}
|
||||
|
||||
fn session_target_from_app_server_thread(thread: AppServerThread) -> Result<ResolvedSessionTarget> {
|
||||
@@ -173,6 +209,35 @@ fn session_target_from_app_server_thread(thread: AppServerThread) -> Result<Reso
|
||||
})
|
||||
}
|
||||
|
||||
fn confirm_session_delete(target: &ResolvedSessionTarget) -> Result<bool> {
|
||||
if !(std::io::stdin().is_terminal() && std::io::stderr().is_terminal()) {
|
||||
return Err(eyre!(
|
||||
"cannot confirm session deletion without an interactive terminal; rerun with --force and a session UUID"
|
||||
));
|
||||
}
|
||||
|
||||
let mut stderr = std::io::stderr().lock();
|
||||
match target.session_name.as_deref() {
|
||||
Some(name) => writeln!(
|
||||
stderr,
|
||||
"Permanently delete session '{name}' ({})?",
|
||||
target.session_id
|
||||
),
|
||||
None => writeln!(stderr, "Permanently delete session {}?", target.session_id),
|
||||
}?;
|
||||
writeln!(
|
||||
stderr,
|
||||
"This cannot be undone. Subagent threads will also be deleted."
|
||||
)?;
|
||||
write!(stderr, "Continue? [y/N]: ")?;
|
||||
stderr.flush()?;
|
||||
|
||||
let mut input = String::new();
|
||||
std::io::stdin().read_line(&mut input)?;
|
||||
let answer = input.trim();
|
||||
Ok(answer.eq_ignore_ascii_case("y") || answer.eq_ignore_ascii_case("yes"))
|
||||
}
|
||||
|
||||
async fn start_app_server_for_archive_command(
|
||||
options: SessionArchiveCommandOptions,
|
||||
) -> Result<AppServerSession> {
|
||||
|
||||
@@ -32,6 +32,7 @@ pub enum SlashCommand {
|
||||
Rename,
|
||||
New,
|
||||
Archive,
|
||||
Delete,
|
||||
Resume,
|
||||
Fork,
|
||||
App,
|
||||
@@ -90,6 +91,7 @@ impl SlashCommand {
|
||||
SlashCommand::Rename => "rename the current thread",
|
||||
SlashCommand::Resume => "resume a saved chat",
|
||||
SlashCommand::Archive => "archive this session and exit",
|
||||
SlashCommand::Delete => "permanently delete this session and exit",
|
||||
SlashCommand::Clear => "clear the terminal and start a new chat",
|
||||
SlashCommand::Fork => "fork the current chat",
|
||||
SlashCommand::App => "continue this session in Codex Desktop",
|
||||
@@ -189,6 +191,7 @@ impl SlashCommand {
|
||||
match self {
|
||||
SlashCommand::New
|
||||
| SlashCommand::Archive
|
||||
| SlashCommand::Delete
|
||||
| SlashCommand::Resume
|
||||
| SlashCommand::Fork
|
||||
| SlashCommand::Init
|
||||
|
||||
Reference in New Issue
Block a user