diff --git a/crates/chat-cli/src/cli/chat/auto_save.rs b/crates/chat-cli/src/cli/chat/auto_save.rs new file mode 100644 index 0000000000..bad184f09c --- /dev/null +++ b/crates/chat-cli/src/cli/chat/auto_save.rs @@ -0,0 +1,61 @@ +use crate::database::settings::Setting; +use crate::os::Os; +use crate::cli::chat::conversation::ConversationState; +use chrono::Local; +use tracing::warn; + +pub struct AutoSaveManager { + session_filename: Option, +} + +impl AutoSaveManager { + pub fn new() -> Self { + Self { + session_filename: None, + } + } + + pub async fn auto_save_if_enabled( + &mut self, + os: &Os, + conversation: &ConversationState, + ) -> Result<(), Box> { + // Check if auto-save is enabled + let auto_save_enabled = os.database.settings.get_bool(Setting::ChatEnableAutoSave).unwrap_or(false); + tracing::info!("Auto-save check: enabled={}", auto_save_enabled); + + if !auto_save_enabled { + return Ok(()); + } + + // Generate filename on first save + if self.session_filename.is_none() { + let pattern = os.database.settings + .get_string(Setting::ChatAutoSavePath) + .unwrap_or_else(|| "auto-save-{timestamp}.json".to_string()); + + let timestamp = Local::now().format("%Y%m%d-%H%M%S"); + let filename = pattern.replace("{timestamp}", ×tamp.to_string()); + tracing::info!("Auto-save: generating filename: {}", filename); + self.session_filename = Some(filename); + } + + // Execute auto-save + if let Some(filename) = &self.session_filename { + tracing::info!("Auto-save: attempting to save to {}", filename); + match serde_json::to_string_pretty(conversation) { + Ok(contents) => { + match os.fs.write(filename, contents).await { + Ok(_) => tracing::info!("Auto-save: successfully saved to {}", filename), + Err(e) => warn!("Auto-save failed: {}", e), + } + } + Err(e) => { + warn!("Auto-save serialization failed: {}", e); + } + } + } + + Ok(()) + } +} diff --git a/crates/chat-cli/src/cli/chat/mod.rs b/crates/chat-cli/src/cli/chat/mod.rs index 81043fdad3..a54c283867 100644 --- a/crates/chat-cli/src/cli/chat/mod.rs +++ b/crates/chat-cli/src/cli/chat/mod.rs @@ -12,6 +12,7 @@ pub mod cli; mod consts; pub mod context; mod conversation; +mod auto_save; mod input_source; mod message; mod parse; @@ -677,6 +678,7 @@ pub struct ChatSession { prompt_ack_rx: std::sync::mpsc::Receiver<()>, /// Additional context to be added to the next user message (e.g., delegate task summaries) pending_additional_context: Option, + auto_save_manager: auto_save::AutoSaveManager, } impl ChatSession { @@ -814,6 +816,7 @@ impl ChatSession { wrap, prompt_ack_rx, pending_additional_context: None, + auto_save_manager: auto_save::AutoSaveManager::new(), }) } @@ -1172,6 +1175,11 @@ impl ChatSession { self.tool_turn_start_time = None; self.reset_user_turn(); + // Auto-save conversation if enabled + if let Err(e) = self.auto_save_manager.auto_save_if_enabled(os, &self.conversation).await { + warn!("Auto-save error: {}", e); + } + self.inner = Some(ChatState::PromptUser { skip_printing_tools: false, }); @@ -3207,6 +3215,11 @@ impl ChatSession { .await; } + // Auto-save conversation if enabled + if let Err(e) = self.auto_save_manager.auto_save_if_enabled(os, &self.conversation).await { + warn!("Auto-save error: {}", e); + } + Ok(ChatState::PromptUser { skip_printing_tools: false, }) diff --git a/crates/chat-cli/src/database/settings.rs b/crates/chat-cli/src/database/settings.rs index 02aec64a9a..43d580ee33 100644 --- a/crates/chat-cli/src/database/settings.rs +++ b/crates/chat-cli/src/database/settings.rs @@ -89,6 +89,10 @@ pub enum Setting { EnabledCheckpoint, #[strum(message = "Enable the delegate tool for subagent management (boolean)")] EnabledDelegate, + #[strum(message = "Enable automatic conversation saving (boolean)")] + ChatEnableAutoSave, + #[strum(message = "Auto-save file path pattern (string)")] + ChatAutoSavePath, #[strum(message = "Specify UI variant to use (string)")] UiMode, } @@ -132,6 +136,8 @@ impl AsRef for Setting { Self::EnabledCheckpoint => "chat.enableCheckpoint", Self::EnabledContextUsageIndicator => "chat.enableContextUsageIndicator", Self::EnabledDelegate => "chat.enableDelegate", + Self::ChatEnableAutoSave => "chat.enableAutoSave", + Self::ChatAutoSavePath => "chat.autoSavePath", Self::UiMode => "chat.uiMode", } } @@ -182,6 +188,9 @@ impl TryFrom<&str> for Setting { "chat.enableTodoList" => Ok(Self::EnabledTodoList), "chat.enableCheckpoint" => Ok(Self::EnabledCheckpoint), "chat.enableContextUsageIndicator" => Ok(Self::EnabledContextUsageIndicator), + "chat.enableDelegate" => Ok(Self::EnabledDelegate), + "chat.enableAutoSave" => Ok(Self::ChatEnableAutoSave), + "chat.autoSavePath" => Ok(Self::ChatAutoSavePath), "chat.uiMode" => Ok(Self::UiMode), _ => Err(DatabaseError::InvalidSetting(value.to_string())), }