feat(daemon): [resume] config map + snapshot_interval_secs with built-in defaults

Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
2026-06-15 15:38:30 +07:00
parent bd36a83db2
commit 1a7d04aab0
+72
View File
@@ -22,6 +22,20 @@ pub struct AppearanceConfig {
pub accent: Option<String>, pub accent: Option<String>,
} }
/// 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<String, Vec<String>>,
}
#[derive(Debug, Clone, Default, Deserialize, Serialize)] #[derive(Debug, Clone, Default, Deserialize, Serialize)]
pub struct Config { pub struct Config {
/// Shell launched for plain (no-command) panels. When unset, the daemon /// Shell launched for plain (no-command) panels. When unset, the daemon
@@ -32,6 +46,11 @@ pub struct Config {
pub terminal: TerminalConfig, pub terminal: TerminalConfig,
#[serde(default)] #[serde(default)]
pub appearance: AppearanceConfig, 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<u64>,
} }
impl Config { impl Config {
@@ -85,6 +104,25 @@ impl Config {
.map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?; .map_err(|e| std::io::Error::new(std::io::ErrorKind::Other, e.to_string()))?;
std::fs::write(path, s) 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<Vec<String>> {
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. /// Resolve the shell to spawn for a plain panel.
@@ -298,4 +336,38 @@ mod tests {
assert_eq!(back.appearance.accent.as_deref(), Some("purple")); assert_eq!(back.appearance.accent.as_deref(), Some("purple"));
let _ = std::fs::remove_file(&path); 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);
}
} }