feat: add bulk delete for session manager (#1693)

* feat: add bulk delete for session manager

* fix: address batch delete review issues

* fix: keep session list in sync after batch delete
This commit is contained in:
Alexlangl
2026-03-30 22:15:57 +08:00
committed by GitHub
parent 4f7ea76347
commit 8e2ffbc845
11 changed files with 960 additions and 218 deletions
+105 -1
View File
@@ -1,7 +1,7 @@
pub mod providers;
pub mod terminal;
use serde::Serialize;
use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use providers::{claude, codex, gemini, openclaw, opencode};
@@ -36,6 +36,25 @@ pub struct SessionMessage {
pub ts: Option<i64>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSessionRequest {
pub provider_id: String,
pub session_id: String,
pub source_path: String,
}
#[derive(Debug, Clone, Serialize)]
#[serde(rename_all = "camelCase")]
pub struct DeleteSessionOutcome {
pub provider_id: String,
pub session_id: String,
pub source_path: String,
pub success: bool,
#[serde(skip_serializing_if = "Option::is_none")]
pub error: Option<String>,
}
pub fn scan_sessions() -> Vec<SessionMeta> {
let (r1, r2, r3, r4, r5) = std::thread::scope(|s| {
let h1 = s.spawn(codex::scan_sessions);
@@ -99,6 +118,16 @@ pub fn delete_session(
delete_session_with_root(provider_id, session_id, Path::new(source_path), &root)
}
pub fn delete_sessions(requests: &[DeleteSessionRequest]) -> Vec<DeleteSessionOutcome> {
collect_delete_session_outcomes(requests, |request| {
delete_session(
&request.provider_id,
&request.session_id,
&request.source_path,
)
})
}
fn delete_session_with_root(
provider_id: &str,
session_id: &str,
@@ -147,6 +176,41 @@ fn canonicalize_existing_path(path: &Path, label: &str) -> Result<PathBuf, Strin
.map_err(|e| format!("Failed to resolve {label} {}: {e}", path.display()))
}
fn collect_delete_session_outcomes<F>(
requests: &[DeleteSessionRequest],
mut deleter: F,
) -> Vec<DeleteSessionOutcome>
where
F: FnMut(&DeleteSessionRequest) -> Result<bool, String>,
{
requests
.iter()
.map(|request| match deleter(request) {
Ok(true) => DeleteSessionOutcome {
provider_id: request.provider_id.clone(),
session_id: request.session_id.clone(),
source_path: request.source_path.clone(),
success: true,
error: None,
},
Ok(false) => DeleteSessionOutcome {
provider_id: request.provider_id.clone(),
session_id: request.session_id.clone(),
source_path: request.source_path.clone(),
success: false,
error: Some("Session was not deleted".to_string()),
},
Err(error) => DeleteSessionOutcome {
provider_id: request.provider_id.clone(),
session_id: request.session_id.clone(),
source_path: request.source_path.clone(),
success: false,
error: Some(error),
},
})
.collect()
}
#[cfg(test)]
mod tests {
use super::*;
@@ -175,4 +239,44 @@ mod tests {
assert!(err.contains("session source not found"));
}
#[test]
fn batch_delete_collects_successes_and_failures_in_order() {
let requests = vec![
DeleteSessionRequest {
provider_id: "codex".to_string(),
session_id: "s1".to_string(),
source_path: "/tmp/s1".to_string(),
},
DeleteSessionRequest {
provider_id: "claude".to_string(),
session_id: "s2".to_string(),
source_path: "/tmp/s2".to_string(),
},
DeleteSessionRequest {
provider_id: "gemini".to_string(),
session_id: "s3".to_string(),
source_path: "/tmp/s3".to_string(),
},
];
let outcomes = collect_delete_session_outcomes(&requests, |request| {
match request.session_id.as_str() {
"s1" => Ok(true),
"s2" => Err("boom".to_string()),
_ => Ok(false),
}
});
assert_eq!(outcomes.len(), 3);
assert!(outcomes[0].success);
assert_eq!(outcomes[0].error, None);
assert!(!outcomes[1].success);
assert_eq!(outcomes[1].error.as_deref(), Some("boom"));
assert!(!outcomes[2].success);
assert_eq!(
outcomes[2].error.as_deref(),
Some("Session was not deleted")
);
}
}