Add interactive deletion of sessions (#2357)

Co-authored-by: Yingjie He <yingjiehe@squareup.com>
This commit is contained in:
Raduan Al-Shedivat
2025-05-25 19:27:15 +02:00
committed by GitHub
parent 7699fc261f
commit 7e28762cac
2 changed files with 73 additions and 28 deletions
+5 -10
View File
@@ -90,17 +90,12 @@ enum SessionCommand {
)] )]
ascending: bool, ascending: bool,
}, },
#[command(about = "Remove sessions")] #[command(about = "Remove sessions. Runs interactively if no ID or regex is provided.")]
Remove { Remove {
#[arg(short, long, help = "session id to be removed", default_value = "")] #[arg(short, long, help = "Session ID to be removed (optional)")]
id: String, id: Option<String>,
#[arg( #[arg(short, long, help = "Regex for removing matched sessions (optional)")]
short, regex: Option<String>,
long,
help = "regex for removing matched session",
default_value = ""
)]
regex: String,
}, },
} }
+68 -18
View File
@@ -1,18 +1,20 @@
use anyhow::{Context, Result}; use anyhow::{Context, Result};
use cliclack::{confirm, multiselect};
use goose::session::info::{get_session_info, SessionInfo, SortOrder}; use goose::session::info::{get_session_info, SessionInfo, SortOrder};
use regex::Regex; use regex::Regex;
use std::fs; use std::fs;
const TRUNCATED_DESC_LENGTH: usize = 60;
pub fn remove_sessions(sessions: Vec<SessionInfo>) -> Result<()> { pub fn remove_sessions(sessions: Vec<SessionInfo>) -> Result<()> {
println!("The following sessions will be removed:"); println!("The following sessions will be removed:");
for session in &sessions { for session in &sessions {
println!("- {}", session.id); println!("- {}", session.id);
} }
let should_delete = let should_delete = confirm("Are you sure you want to delete these sessions?")
cliclack::confirm("Are you sure you want to delete all these sessions? (yes/no):") .initial_value(false)
.initial_value(true) .interact()?;
.interact()?;
if should_delete { if should_delete {
for session in sessions { for session in sessions {
@@ -27,8 +29,50 @@ pub fn remove_sessions(sessions: Vec<SessionInfo>) -> Result<()> {
Ok(()) Ok(())
} }
pub fn handle_session_remove(id: String, regex_string: String) -> Result<()> { fn prompt_interactive_session_selection(sessions: &[SessionInfo]) -> Result<Vec<SessionInfo>> {
let sessions = match get_session_info(SortOrder::Descending) { 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<String, SessionInfo> = 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<String> = selector.interact()?;
let selected_sessions: Vec<SessionInfo> = selected_display_texts
.into_iter()
.filter_map(|text| display_map.get(&text).cloned())
.collect();
Ok(selected_sessions)
}
pub fn handle_session_remove(id: Option<String>, regex_string: Option<String>) -> Result<()> {
let all_sessions = match get_session_info(SortOrder::Descending) {
Ok(sessions) => sessions, Ok(sessions) => sessions,
Err(e) => { Err(e) => {
tracing::error!("Failed to retrieve sessions: {:?}", 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<SessionInfo>; let matched_sessions: Vec<SessionInfo>;
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()]; matched_sessions = vec![session.clone()];
} else { } else {
return Err(anyhow::anyhow!("Session '{}' not found.", id)); return Err(anyhow::anyhow!("Session '{}' not found.", id_val));
} }
} else if !regex_string.is_empty() { } else if let Some(regex_val) = regex_string {
let session_regex = Regex::new(&regex_string) let session_regex = Regex::new(&regex_val)
.with_context(|| format!("Invalid regex pattern '{}'", regex_string))?; .with_context(|| format!("Invalid regex pattern '{}'", regex_val))?;
matched_sessions = sessions
matched_sessions = all_sessions
.into_iter() .into_iter()
.filter(|session| session_regex.is_match(&session.id)) .filter(|session| session_regex.is_match(&session.id))
.collect(); .collect();
if matched_sessions.is_empty() { if matched_sessions.is_empty() {
println!( println!("Regex string '{}' does not match any sessions", regex_val);
"Regex string '{}' does not match any sessions",
regex_string
);
return Ok(()); return Ok(());
} }
} else { } 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) remove_sessions(matched_sessions)