From 1a7d04aab08b05387f9a482eff496b4b70410b8f Mon Sep 17 00:00:00 2001 From: Vassiliy Yegorov Date: Mon, 15 Jun 2026 15:38:30 +0700 Subject: [PATCH] feat(daemon): [resume] config map + snapshot_interval_secs with built-in defaults Co-Authored-By: Claude Sonnet 4.6 --- crates/spaceshd/src/config.rs | 72 +++++++++++++++++++++++++++++++++++ 1 file changed, 72 insertions(+) diff --git a/crates/spaceshd/src/config.rs b/crates/spaceshd/src/config.rs index b659f10..ee5adc1 100644 --- a/crates/spaceshd/src/config.rs +++ b/crates/spaceshd/src/config.rs @@ -22,6 +22,20 @@ pub struct AppearanceConfig { pub accent: Option, } +/// Built-in resume args for known agents, used when config has no override. +/// (command basename, resume args) +const DEFAULT_RESUME: &[(&str, &[&str])] = &[ + ("claude", &["--continue"]), + ("codex", &["resume"]), +]; + +#[derive(Debug, Clone, Default, Deserialize, Serialize)] +pub struct ResumeConfig { + /// command basename -> args that continue its previous session. + #[serde(default)] + pub commands: std::collections::HashMap>, +} + #[derive(Debug, Clone, Default, Deserialize, Serialize)] pub struct Config { /// Shell launched for plain (no-command) panels. When unset, the daemon @@ -32,6 +46,11 @@ pub struct Config { pub terminal: TerminalConfig, #[serde(default)] pub appearance: AppearanceConfig, + #[serde(default)] + pub resume: ResumeConfig, + /// How often (seconds) the daemon dumps changed grids to disk. + #[serde(default, skip_serializing_if = "Option::is_none")] + pub snapshot_interval_secs: Option, } impl Config { @@ -85,6 +104,25 @@ impl Config { .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; std::fs::write(path, s) } + + /// Resume args for a command, by basename: user map → built-in default → None. + pub fn resume_args(&self, command: &str) -> Option> { + let base = std::path::Path::new(command) + .file_name() + .map(|s| s.to_string_lossy().to_string()) + .unwrap_or_else(|| command.to_string()); + if let Some(args) = self.resume.commands.get(&base) { + return Some(args.clone()); + } + DEFAULT_RESUME.iter() + .find(|(name, _)| *name == base) + .map(|(_, args)| args.iter().map(|s| s.to_string()).collect()) + } + + /// Snapshot dump cadence in seconds (config → default 5, clamped to [1, 3600]). + pub fn snapshot_interval_secs(&self) -> u64 { + self.snapshot_interval_secs.unwrap_or(5).clamp(1, 3600) + } } /// Resolve the shell to spawn for a plain panel. @@ -298,4 +336,38 @@ mod tests { assert_eq!(back.appearance.accent.as_deref(), Some("purple")); let _ = std::fs::remove_file(&path); } + + #[test] + fn resume_args_user_then_default_then_none() { + let mut c = Config::default(); + // built-in defaults present without any config + assert_eq!(c.resume_args("claude").as_deref(), Some(&["--continue".to_string()][..])); + assert_eq!(c.resume_args("codex").as_deref(), Some(&["resume".to_string()][..])); + // a path is reduced to its basename before lookup + assert_eq!(c.resume_args("/usr/local/bin/claude").as_deref(), Some(&["--continue".to_string()][..])); + // unknown command → None + assert_eq!(c.resume_args("bash"), None); + // user override wins over the default + c.resume.commands.insert("claude".into(), vec!["--resume".into(), "last".into()]); + assert_eq!(c.resume_args("claude"), Some(vec!["--resume".into(), "last".into()])); + } + + #[test] + fn snapshot_interval_defaults_to_5s() { + let c = Config::default(); + assert_eq!(c.snapshot_interval_secs(), 5); + } + + #[test] + fn parses_resume_table_and_interval() { + let dir = std::env::temp_dir().join(format!("spacesh-cfg-resume-{}", std::process::id())); + std::fs::create_dir_all(&dir).unwrap(); + let path = dir.join("config.toml"); + std::fs::write(&path, + "snapshot_interval_secs = 10\n[resume.commands]\ngemini = [\"--resume\"]\n").unwrap(); + let c = Config::from_path(&path); + assert_eq!(c.snapshot_interval_secs(), 10); + assert_eq!(c.resume_args("gemini"), Some(vec!["--resume".into()])); + let _ = std::fs::remove_file(&path); + } }