mirror of
https://github.com/affaan-m/everything-claude-code.git
synced 2026-04-09 10:53:34 +08:00
feat: add ecc2 dashboard worktree pruning
This commit is contained in:
@@ -52,6 +52,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> {
|
|||||||
(_, KeyCode::Char('s')) => dashboard.stop_selected().await,
|
(_, KeyCode::Char('s')) => dashboard.stop_selected().await,
|
||||||
(_, KeyCode::Char('u')) => dashboard.resume_selected().await,
|
(_, KeyCode::Char('u')) => dashboard.resume_selected().await,
|
||||||
(_, KeyCode::Char('x')) => dashboard.cleanup_selected_worktree().await,
|
(_, KeyCode::Char('x')) => dashboard.cleanup_selected_worktree().await,
|
||||||
|
(_, KeyCode::Char('X')) => dashboard.prune_inactive_worktrees().await,
|
||||||
(_, KeyCode::Char('d')) => dashboard.delete_selected_session().await,
|
(_, KeyCode::Char('d')) => dashboard.delete_selected_session().await,
|
||||||
(_, KeyCode::Char('r')) => dashboard.refresh(),
|
(_, KeyCode::Char('r')) => dashboard.refresh(),
|
||||||
(_, KeyCode::Char('?')) => dashboard.toggle_help(),
|
(_, KeyCode::Char('?')) => dashboard.toggle_help(),
|
||||||
|
|||||||
@@ -456,7 +456,7 @@ impl Dashboard {
|
|||||||
|
|
||||||
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
fn render_status_bar(&self, frame: &mut Frame, area: Rect) {
|
||||||
let text = format!(
|
let text = format!(
|
||||||
" [n]ew session [a]ssign re[b]alance global re[B]alance dra[i]n inbox [g]lobal dispatch coordinate [G]lobal [v]iew diff toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ",
|
" [n]ew session [a]ssign re[b]alance global re[B]alance dra[i]n inbox [g]lobal dispatch coordinate [G]lobal [v]iew diff toggle [p]olicy [,/.] dispatch limit [s]top [u]resume [x]cleanup prune inactive [X] [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ",
|
||||||
self.layout_label()
|
self.layout_label()
|
||||||
);
|
);
|
||||||
let text = if let Some(note) = self.operator_note.as_ref() {
|
let text = if let Some(note) = self.operator_note.as_ref() {
|
||||||
@@ -513,6 +513,7 @@ impl Dashboard {
|
|||||||
" s Stop selected session",
|
" s Stop selected session",
|
||||||
" u Resume selected session",
|
" u Resume selected session",
|
||||||
" x Cleanup selected worktree",
|
" x Cleanup selected worktree",
|
||||||
|
" X Prune inactive worktrees globally",
|
||||||
" d Delete selected inactive session",
|
" d Delete selected inactive session",
|
||||||
" Tab Next pane",
|
" Tab Next pane",
|
||||||
" S-Tab Previous pane",
|
" S-Tab Previous pane",
|
||||||
@@ -1106,6 +1107,32 @@ impl Dashboard {
|
|||||||
));
|
));
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pub async fn prune_inactive_worktrees(&mut self) {
|
||||||
|
match manager::prune_inactive_worktrees(&self.db).await {
|
||||||
|
Ok(outcome) => {
|
||||||
|
self.refresh();
|
||||||
|
if outcome.cleaned_session_ids.is_empty() {
|
||||||
|
self.set_operator_note("no inactive worktrees to prune".to_string());
|
||||||
|
} else if outcome.active_with_worktree_ids.is_empty() {
|
||||||
|
self.set_operator_note(format!(
|
||||||
|
"pruned {} inactive worktree(s)",
|
||||||
|
outcome.cleaned_session_ids.len()
|
||||||
|
));
|
||||||
|
} else {
|
||||||
|
self.set_operator_note(format!(
|
||||||
|
"pruned {} inactive worktree(s); skipped {} active session(s)",
|
||||||
|
outcome.cleaned_session_ids.len(),
|
||||||
|
outcome.active_with_worktree_ids.len()
|
||||||
|
));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Err(error) => {
|
||||||
|
tracing::warn!("Failed to prune inactive worktrees: {error}");
|
||||||
|
self.set_operator_note(format!("prune inactive worktrees failed: {error}"));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
pub async fn delete_selected_session(&mut self) {
|
pub async fn delete_selected_session(&mut self) {
|
||||||
let Some(session) = self.sessions.get(self.selected_session) else {
|
let Some(session) = self.sessions.get(self.selected_session) else {
|
||||||
return;
|
return;
|
||||||
@@ -2936,6 +2963,108 @@ mod tests {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prune_inactive_worktrees_sets_operator_note_when_clear() -> Result<()> {
|
||||||
|
let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
|
||||||
|
let db = StateStore::open(&db_path)?;
|
||||||
|
let now = Utc::now();
|
||||||
|
|
||||||
|
db.insert_session(&Session {
|
||||||
|
id: "running-1".to_string(),
|
||||||
|
task: "keep alive".to_string(),
|
||||||
|
agent_type: "claude".to_string(),
|
||||||
|
working_dir: PathBuf::from("/tmp"),
|
||||||
|
state: SessionState::Running,
|
||||||
|
pid: None,
|
||||||
|
worktree: None,
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
metrics: SessionMetrics::default(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let dashboard_store = StateStore::open(&db_path)?;
|
||||||
|
let mut dashboard = Dashboard::new(dashboard_store, Config::default());
|
||||||
|
dashboard.prune_inactive_worktrees().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some("no inactive worktrees to prune")
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_file(db_path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
|
#[tokio::test]
|
||||||
|
async fn prune_inactive_worktrees_reports_pruned_and_skipped_counts() -> Result<()> {
|
||||||
|
let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
|
||||||
|
let db = StateStore::open(&db_path)?;
|
||||||
|
let now = Utc::now();
|
||||||
|
let active_path = std::env::temp_dir().join(format!("ecc2-active-{}", Uuid::new_v4()));
|
||||||
|
let stopped_path = std::env::temp_dir().join(format!("ecc2-stopped-{}", Uuid::new_v4()));
|
||||||
|
std::fs::create_dir_all(&active_path)?;
|
||||||
|
std::fs::create_dir_all(&stopped_path)?;
|
||||||
|
|
||||||
|
db.insert_session(&Session {
|
||||||
|
id: "running-1".to_string(),
|
||||||
|
task: "keep worktree".to_string(),
|
||||||
|
agent_type: "claude".to_string(),
|
||||||
|
working_dir: active_path.clone(),
|
||||||
|
state: SessionState::Running,
|
||||||
|
pid: None,
|
||||||
|
worktree: Some(WorktreeInfo {
|
||||||
|
path: active_path.clone(),
|
||||||
|
branch: "ecc/running-1".to_string(),
|
||||||
|
base_branch: "main".to_string(),
|
||||||
|
}),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
metrics: SessionMetrics::default(),
|
||||||
|
})?;
|
||||||
|
db.insert_session(&Session {
|
||||||
|
id: "stopped-1".to_string(),
|
||||||
|
task: "prune me".to_string(),
|
||||||
|
agent_type: "claude".to_string(),
|
||||||
|
working_dir: stopped_path.clone(),
|
||||||
|
state: SessionState::Stopped,
|
||||||
|
pid: None,
|
||||||
|
worktree: Some(WorktreeInfo {
|
||||||
|
path: stopped_path.clone(),
|
||||||
|
branch: "ecc/stopped-1".to_string(),
|
||||||
|
base_branch: "main".to_string(),
|
||||||
|
}),
|
||||||
|
created_at: now,
|
||||||
|
updated_at: now,
|
||||||
|
metrics: SessionMetrics::default(),
|
||||||
|
})?;
|
||||||
|
|
||||||
|
let dashboard_store = StateStore::open(&db_path)?;
|
||||||
|
let mut dashboard = Dashboard::new(dashboard_store, Config::default());
|
||||||
|
dashboard.prune_inactive_worktrees().await;
|
||||||
|
|
||||||
|
assert_eq!(
|
||||||
|
dashboard.operator_note.as_deref(),
|
||||||
|
Some("pruned 1 inactive worktree(s); skipped 1 active session(s)")
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
db.get_session("stopped-1")?
|
||||||
|
.expect("stopped session should exist")
|
||||||
|
.worktree
|
||||||
|
.is_none()
|
||||||
|
);
|
||||||
|
assert!(
|
||||||
|
db.get_session("running-1")?
|
||||||
|
.expect("running session should exist")
|
||||||
|
.worktree
|
||||||
|
.is_some()
|
||||||
|
);
|
||||||
|
|
||||||
|
let _ = std::fs::remove_dir_all(active_path);
|
||||||
|
let _ = std::fs::remove_dir_all(stopped_path);
|
||||||
|
let _ = std::fs::remove_file(db_path);
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::test]
|
#[tokio::test]
|
||||||
async fn delete_selected_session_removes_inactive_session() -> Result<()> {
|
async fn delete_selected_session_removes_inactive_session() -> Result<()> {
|
||||||
let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
|
let db_path = std::env::temp_dir().join(format!("ecc2-dashboard-{}.db", Uuid::new_v4()));
|
||||||
|
|||||||
Reference in New Issue
Block a user