From 7e28762cac106ebfbc2a5ed12b41f6f1947c5d39 Mon Sep 17 00:00:00 2001 From: Raduan Al-Shedivat <88370223+dbraduan@users.noreply.github.com> Date: Sun, 25 May 2025 19:27:15 +0200 Subject: [PATCH] Add interactive deletion of sessions (#2357) Co-authored-by: Yingjie He --- crates/goose-cli/src/cli.rs | 15 ++--- crates/goose-cli/src/commands/session.rs | 86 +++++++++++++++++++----- 2 files changed, 73 insertions(+), 28 deletions(-) diff --git a/crates/goose-cli/src/cli.rs b/crates/goose-cli/src/cli.rs index c4511d67d3..5430cac68c 100644 --- a/crates/goose-cli/src/cli.rs +++ b/crates/goose-cli/src/cli.rs @@ -90,17 +90,12 @@ enum SessionCommand { )] ascending: bool, }, - #[command(about = "Remove sessions")] + #[command(about = "Remove sessions. Runs interactively if no ID or regex is provided.")] Remove { - #[arg(short, long, help = "session id to be removed", default_value = "")] - id: String, - #[arg( - short, - long, - help = "regex for removing matched session", - default_value = "" - )] - regex: String, + #[arg(short, long, help = "Session ID to be removed (optional)")] + id: Option, + #[arg(short, long, help = "Regex for removing matched sessions (optional)")] + regex: Option, }, } diff --git a/crates/goose-cli/src/commands/session.rs b/crates/goose-cli/src/commands/session.rs index 5d8f8bed92..add1572dfe 100644 --- a/crates/goose-cli/src/commands/session.rs +++ b/crates/goose-cli/src/commands/session.rs @@ -1,18 +1,20 @@ use anyhow::{Context, Result}; +use cliclack::{confirm, multiselect}; use goose::session::info::{get_session_info, SessionInfo, SortOrder}; use regex::Regex; use std::fs; +const TRUNCATED_DESC_LENGTH: usize = 60; + pub fn remove_sessions(sessions: Vec) -> Result<()> { println!("The following sessions will be removed:"); for session in &sessions { println!("- {}", session.id); } - let should_delete = - cliclack::confirm("Are you sure you want to delete all these sessions? (yes/no):") - .initial_value(true) - .interact()?; + let should_delete = confirm("Are you sure you want to delete these sessions?") + .initial_value(false) + .interact()?; if should_delete { for session in sessions { @@ -27,8 +29,50 @@ pub fn remove_sessions(sessions: Vec) -> Result<()> { Ok(()) } -pub fn handle_session_remove(id: String, regex_string: String) -> Result<()> { - let sessions = match get_session_info(SortOrder::Descending) { +fn prompt_interactive_session_selection(sessions: &[SessionInfo]) -> Result> { + if sessions.is_empty() { + println!("No sessions to delete."); + return Ok(vec![]); + } + + let mut selector = multiselect( + "Select sessions to delete (use spacebar, Enter to confirm, Ctrl+C to cancel):", + ); + + let display_map: std::collections::HashMap = sessions + .iter() + .map(|s| { + let desc = if s.metadata.description.is_empty() { + "(no description)" + } else { + &s.metadata.description + }; + let truncated_desc = if desc.len() > TRUNCATED_DESC_LENGTH { + format!("{}...", &desc[..TRUNCATED_DESC_LENGTH - 3]) + } else { + desc.to_string() + }; + let display_text = format!("{} - {} ({})", s.modified, truncated_desc, s.id); + (display_text, s.clone()) + }) + .collect(); + + for display_text in display_map.keys() { + selector = selector.item(display_text.clone(), display_text.clone(), ""); + } + + let selected_display_texts: Vec = selector.interact()?; + + let selected_sessions: Vec = selected_display_texts + .into_iter() + .filter_map(|text| display_map.get(&text).cloned()) + .collect(); + + Ok(selected_sessions) +} + +pub fn handle_session_remove(id: Option, regex_string: Option) -> Result<()> { + let all_sessions = match get_session_info(SortOrder::Descending) { Ok(sessions) => sessions, Err(e) => { tracing::error!("Failed to retrieve sessions: {:?}", e); @@ -37,29 +81,35 @@ pub fn handle_session_remove(id: String, regex_string: String) -> Result<()> { }; let matched_sessions: Vec; - if !id.is_empty() { - if let Some(session) = sessions.iter().find(|s| s.id == id) { + + if let Some(id_val) = id { + if let Some(session) = all_sessions.iter().find(|s| s.id == id_val) { matched_sessions = vec![session.clone()]; } else { - return Err(anyhow::anyhow!("Session '{}' not found.", id)); + return Err(anyhow::anyhow!("Session '{}' not found.", id_val)); } - } else if !regex_string.is_empty() { - let session_regex = Regex::new(®ex_string) - .with_context(|| format!("Invalid regex pattern '{}'", regex_string))?; - matched_sessions = sessions + } else if let Some(regex_val) = regex_string { + let session_regex = Regex::new(®ex_val) + .with_context(|| format!("Invalid regex pattern '{}'", regex_val))?; + + matched_sessions = all_sessions .into_iter() .filter(|session| session_regex.is_match(&session.id)) .collect(); if matched_sessions.is_empty() { - println!( - "Regex string '{}' does not match any sessions", - regex_string - ); + println!("Regex string '{}' does not match any sessions", regex_val); return Ok(()); } } else { - return Err(anyhow::anyhow!("Neither --regex nor --id flags provided.")); + if all_sessions.is_empty() { + return Err(anyhow::anyhow!("No sessions found.")); + } + matched_sessions = prompt_interactive_session_selection(&all_sessions)?; + } + + if matched_sessions.is_empty() { + return Ok(()); } remove_sessions(matched_sessions)