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:
@@ -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);
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user