use super::session::SessionPaths;
use super::types::ChatMessage;
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::{Path, PathBuf};
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct TeammateSnapshotPersist {
pub name: String,
pub role: String,
pub prompt: String,
pub worktree: bool,
pub worktree_branch: Option<String>,
pub inherit_permissions: bool,
pub status: crate::command::chat::teammate::TeammateStatusPersist,
#[serde(default)]
pub pending_user_messages: Vec<ChatMessage>,
pub tool_calls_count: usize,
pub current_tool: Option<String>,
pub work_done: bool,
}
pub fn sanitize_filename(name: &str) -> String {
let mut out = String::with_capacity(name.len());
for c in name.chars() {
if c.is_whitespace() || matches!(c, '/' | '\\' | ':' | '*' | '?' | '"' | '<' | '>' | '|') {
out.push('_');
} else {
out.push(c);
}
}
if out.chars().count() > 64 {
out = out.chars().take(64).collect();
}
if out.is_empty() {
out.push('_');
}
out
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SubAgentSnapshotPersist {
pub id: String,
pub description: String,
pub mode: String,
pub status: String,
pub current_tool: Option<String>,
pub tool_calls_count: usize,
pub current_round: usize,
pub started_at_epoch: u64,
#[serde(default)]
pub transcript_file: String,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct PlanStatePersist {
pub active: bool,
pub plan_file_path: Option<String>,
pub plan_content: Option<String>,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SessionHookPersist {
pub event: crate::command::chat::infra::hook::HookEvent,
pub definition: crate::command::chat::infra::hook::HookDef,
}
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct SandboxStatePersist {
pub extra_safe_dirs: Vec<PathBuf>,
}
fn save_session_json<T: Serialize + ?Sized>(path: &Path, data: &T) -> bool {
match serde_json::to_string_pretty(data) {
Ok(json) => fs::write(path, json).is_ok(),
Err(_) => false,
}
}
fn load_session_json<T: serde::de::DeserializeOwned>(path: &Path) -> Option<T> {
let content = fs::read_to_string(path).ok()?;
serde_json::from_str(&content).ok()
}
pub fn save_teammates_state(session_id: &str, data: &[TeammateSnapshotPersist]) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.teammates_file(), data)
}
pub fn load_teammates_state(session_id: &str) -> Option<Vec<TeammateSnapshotPersist>> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.teammates_file())
}
pub fn save_subagents_state(session_id: &str, data: &[SubAgentSnapshotPersist]) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.subagents_file(), data)
}
#[allow(dead_code)]
pub fn load_subagents_state(session_id: &str) -> Option<Vec<SubAgentSnapshotPersist>> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.subagents_file())
}
pub fn save_tasks_state(
session_id: &str,
data: &[crate::command::chat::tools::task::AgentTask],
) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.tasks_file(), data)
}
pub fn load_tasks_state(
session_id: &str,
) -> Option<Vec<crate::command::chat::tools::task::AgentTask>> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.tasks_file())
}
pub fn save_todos_state(
session_id: &str,
data: &[crate::command::chat::tools::todo::TodoItem],
) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.todos_file(), data)
}
pub fn load_todos_state(
session_id: &str,
) -> Option<Vec<crate::command::chat::tools::todo::TodoItem>> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.todos_file())
}
pub fn save_plan_state(session_id: &str, data: &PlanStatePersist) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.plan_file(), data)
}
pub fn load_plan_state(session_id: &str) -> Option<PlanStatePersist> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.plan_file())
}
pub fn save_skills_state(
session_id: &str,
data: &std::collections::HashMap<String, crate::command::chat::agent::compact::InvokedSkill>,
) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.skills_file(), data)
}
pub fn load_skills_state(
session_id: &str,
) -> Option<std::collections::HashMap<String, crate::command::chat::agent::compact::InvokedSkill>> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.skills_file())
}
pub fn save_hooks_state(session_id: &str, data: &[SessionHookPersist]) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.hooks_file(), data)
}
pub fn load_hooks_state(session_id: &str) -> Option<Vec<SessionHookPersist>> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.hooks_file())
}
pub fn save_sandbox_state(session_id: &str, data: &SandboxStatePersist) -> bool {
let paths = SessionPaths::new(session_id);
let _ = paths.ensure_dir();
save_session_json(&paths.sandbox_file(), data)
}
pub fn load_sandbox_state(session_id: &str) -> Option<SandboxStatePersist> {
let paths = SessionPaths::new(session_id);
load_session_json(&paths.sandbox_file())
}