From a7309481f45158dc3ee37b4d38f43aa0aaae486f Mon Sep 17 00:00:00 2001 From: Affaan Mustafa Date: Tue, 7 Apr 2026 13:18:10 -0700 Subject: [PATCH] feat: persist ecc2 auto-dispatch policy --- ecc2/src/config/mod.rs | 42 ++++++++++++++++++++++++++++++++++++--- ecc2/src/tui/app.rs | 1 + ecc2/src/tui/dashboard.rs | 24 +++++++++++++++++++++- 3 files changed, 63 insertions(+), 4 deletions(-) diff --git a/ecc2/src/config/mod.rs b/ecc2/src/config/mod.rs index 1a0b6cea..d6abb4b3 100644 --- a/ecc2/src/config/mod.rs +++ b/ecc2/src/config/mod.rs @@ -73,11 +73,15 @@ impl Config { block: 0.85, }; - pub fn load() -> Result { - let config_path = dirs::home_dir() + pub fn config_path() -> PathBuf { + dirs::home_dir() .unwrap_or_else(|| PathBuf::from(".")) .join(".claude") - .join("ecc2.toml"); + .join("ecc2.toml") + } + + pub fn load() -> Result { + let config_path = Self::config_path(); if config_path.exists() { let content = std::fs::read_to_string(&config_path)?; @@ -87,6 +91,20 @@ impl Config { Ok(Config::default()) } } + + pub fn save(&self) -> Result<()> { + self.save_to_path(&Self::config_path()) + } + + pub fn save_to_path(&self, path: &std::path::Path) -> Result<()> { + if let Some(parent) = path.parent() { + std::fs::create_dir_all(parent)?; + } + + let content = toml::to_string_pretty(self)?; + std::fs::write(path, content)?; + Ok(()) + } } impl Default for RiskThresholds { @@ -98,6 +116,7 @@ impl Default for RiskThresholds { #[cfg(test)] mod tests { use super::{Config, PaneLayout}; + use uuid::Uuid; #[test] fn default_includes_positive_budget_thresholds() { @@ -153,4 +172,21 @@ theme = "Dark" fn default_risk_thresholds_are_applied() { assert_eq!(Config::default().risk_thresholds, Config::RISK_THRESHOLDS); } + + #[test] + fn save_round_trips_auto_dispatch_settings() { + let path = std::env::temp_dir().join(format!("ecc2-config-{}.toml", Uuid::new_v4())); + let mut config = Config::default(); + config.auto_dispatch_unread_handoffs = true; + config.auto_dispatch_limit_per_session = 9; + + config.save_to_path(&path).unwrap(); + let content = std::fs::read_to_string(&path).unwrap(); + let loaded: Config = toml::from_str(&content).unwrap(); + + assert!(loaded.auto_dispatch_unread_handoffs); + assert_eq!(loaded.auto_dispatch_limit_per_session, 9); + + let _ = std::fs::remove_file(path); + } } diff --git a/ecc2/src/tui/app.rs b/ecc2/src/tui/app.rs index 5b86edfd..d6be129f 100644 --- a/ecc2/src/tui/app.rs +++ b/ecc2/src/tui/app.rs @@ -43,6 +43,7 @@ pub async fn run(db: StateStore, cfg: Config) -> Result<()> { (_, KeyCode::Char('b')) => dashboard.rebalance_selected_team().await, (_, KeyCode::Char('i')) => dashboard.drain_inbox_selected().await, (_, KeyCode::Char('g')) => dashboard.auto_dispatch_backlog().await, + (_, KeyCode::Char('p')) => dashboard.toggle_auto_dispatch_policy(), (_, KeyCode::Char('s')) => dashboard.stop_selected().await, (_, KeyCode::Char('u')) => dashboard.resume_selected().await, (_, KeyCode::Char('x')) => dashboard.cleanup_selected_worktree().await, diff --git a/ecc2/src/tui/dashboard.rs b/ecc2/src/tui/dashboard.rs index dcbb412d..628faf35 100644 --- a/ecc2/src/tui/dashboard.rs +++ b/ecc2/src/tui/dashboard.rs @@ -400,7 +400,7 @@ impl Dashboard { fn render_status_bar(&self, frame: &mut Frame, area: Rect) { let text = format!( - " [n]ew session [a]ssign re[b]alance dra[i]n inbox [g]lobal dispatch [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 dra[i]n inbox [g]lobal dispatch toggle [p]olicy [s]top [u]resume [x]cleanup [d]elete [r]efresh [Tab] switch pane [j/k] scroll [+/-] resize [{}] layout [?] help [q]uit ", self.layout_label() ); let text = if let Some(note) = self.operator_note.as_ref() { @@ -449,6 +449,7 @@ impl Dashboard { " b Rebalance backed-up delegate inboxes for selected lead", " i Drain unread task handoffs from selected session inbox", " g Auto-dispatch unread handoffs across lead sessions", + " p Toggle daemon auto-dispatch policy and persist config", " s Stop selected session", " u Resume selected session", " x Cleanup selected worktree", @@ -910,6 +911,27 @@ impl Dashboard { self.show_help = !self.show_help; } + pub fn toggle_auto_dispatch_policy(&mut self) { + self.cfg.auto_dispatch_unread_handoffs = !self.cfg.auto_dispatch_unread_handoffs; + match self.cfg.save() { + Ok(()) => { + let state = if self.cfg.auto_dispatch_unread_handoffs { + "enabled" + } else { + "disabled" + }; + self.set_operator_note(format!( + "daemon auto-dispatch {state} | saved to {}", + crate::config::Config::config_path().display() + )); + } + Err(error) => { + self.cfg.auto_dispatch_unread_handoffs = !self.cfg.auto_dispatch_unread_handoffs; + self.set_operator_note(format!("failed to persist auto-dispatch policy: {error}")); + } + } + } + pub async fn tick(&mut self) { loop { match self.output_rx.try_recv() {