Skip to content

Commit 783eb17

Browse files
committed
feat: add auto-save functionality for chat conversations
- Add chat.enableAutoSave and chat.autoSavePath settings - Implement AutoSaveManager for session-based auto-saving - Auto-save triggers after AI response completion - Silent error handling to avoid interrupting conversations - Opt-in by default (disabled) Closes #3322
1 parent 13babcc commit 783eb17

File tree

3 files changed

+83
-0
lines changed

3 files changed

+83
-0
lines changed
Lines changed: 61 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,61 @@
1+
use crate::database::settings::Setting;
2+
use crate::os::Os;
3+
use crate::cli::chat::conversation::ConversationState;
4+
use chrono::Local;
5+
use tracing::warn;
6+
7+
pub struct AutoSaveManager {
8+
session_filename: Option<String>,
9+
}
10+
11+
impl AutoSaveManager {
12+
pub fn new() -> Self {
13+
Self {
14+
session_filename: None,
15+
}
16+
}
17+
18+
pub async fn auto_save_if_enabled(
19+
&mut self,
20+
os: &Os,
21+
conversation: &ConversationState,
22+
) -> Result<(), Box<dyn std::error::Error>> {
23+
// Check if auto-save is enabled
24+
let auto_save_enabled = os.database.settings.get_bool(Setting::ChatEnableAutoSave).unwrap_or(false);
25+
tracing::info!("Auto-save check: enabled={}", auto_save_enabled);
26+
27+
if !auto_save_enabled {
28+
return Ok(());
29+
}
30+
31+
// Generate filename on first save
32+
if self.session_filename.is_none() {
33+
let pattern = os.database.settings
34+
.get_string(Setting::ChatAutoSavePath)
35+
.unwrap_or_else(|| "auto-save-{timestamp}.json".to_string());
36+
37+
let timestamp = Local::now().format("%Y%m%d-%H%M%S");
38+
let filename = pattern.replace("{timestamp}", &timestamp.to_string());
39+
tracing::info!("Auto-save: generating filename: {}", filename);
40+
self.session_filename = Some(filename);
41+
}
42+
43+
// Execute auto-save
44+
if let Some(filename) = &self.session_filename {
45+
tracing::info!("Auto-save: attempting to save to {}", filename);
46+
match serde_json::to_string_pretty(conversation) {
47+
Ok(contents) => {
48+
match os.fs.write(filename, contents).await {
49+
Ok(_) => tracing::info!("Auto-save: successfully saved to {}", filename),
50+
Err(e) => warn!("Auto-save failed: {}", e),
51+
}
52+
}
53+
Err(e) => {
54+
warn!("Auto-save serialization failed: {}", e);
55+
}
56+
}
57+
}
58+
59+
Ok(())
60+
}
61+
}

crates/chat-cli/src/cli/chat/mod.rs

Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@ pub mod cli;
99
mod consts;
1010
pub mod context;
1111
mod conversation;
12+
mod auto_save;
1213
mod input_source;
1314
mod message;
1415
mod parse;
@@ -606,6 +607,7 @@ pub struct ChatSession {
606607
ctrlc_rx: broadcast::Receiver<()>,
607608
wrap: Option<WrapMode>,
608609
prompt_ack_rx: std::sync::mpsc::Receiver<()>,
610+
auto_save_manager: auto_save::AutoSaveManager,
609611
}
610612

611613
impl ChatSession {
@@ -742,6 +744,7 @@ impl ChatSession {
742744
ctrlc_rx,
743745
wrap,
744746
prompt_ack_rx,
747+
auto_save_manager: auto_save::AutoSaveManager::new(),
745748
})
746749
}
747750

@@ -1103,6 +1106,11 @@ impl ChatSession {
11031106
self.tool_turn_start_time = None;
11041107
self.reset_user_turn();
11051108

1109+
// Auto-save conversation if enabled
1110+
if let Err(e) = self.auto_save_manager.auto_save_if_enabled(os, &self.conversation).await {
1111+
warn!("Auto-save error: {}", e);
1112+
}
1113+
11061114
self.inner = Some(ChatState::PromptUser {
11071115
skip_printing_tools: false,
11081116
});
@@ -3134,6 +3142,11 @@ impl ChatSession {
31343142
.await;
31353143
}
31363144

3145+
// Auto-save conversation if enabled
3146+
if let Err(e) = self.auto_save_manager.auto_save_if_enabled(os, &self.conversation).await {
3147+
warn!("Auto-save error: {}", e);
3148+
}
3149+
31373150
Ok(ChatState::PromptUser {
31383151
skip_printing_tools: false,
31393152
})

crates/chat-cli/src/database/settings.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -88,6 +88,10 @@ pub enum Setting {
8888
EnabledCheckpoint,
8989
#[strum(message = "Enable the delegate tool for subagent management (boolean)")]
9090
EnabledDelegate,
91+
#[strum(message = "Enable automatic conversation saving (boolean)")]
92+
ChatEnableAutoSave,
93+
#[strum(message = "Auto-save file path pattern (string)")]
94+
ChatAutoSavePath,
9195
#[strum(message = "Specify UI variant to use (string)")]
9296
UiMode,
9397
}
@@ -131,6 +135,8 @@ impl AsRef<str> for Setting {
131135
Self::EnabledCheckpoint => "chat.enableCheckpoint",
132136
Self::EnabledContextUsageIndicator => "chat.enableContextUsageIndicator",
133137
Self::EnabledDelegate => "chat.enableDelegate",
138+
Self::ChatEnableAutoSave => "chat.enableAutoSave",
139+
Self::ChatAutoSavePath => "chat.autoSavePath",
134140
Self::UiMode => "chat.uiMode",
135141
}
136142
}
@@ -181,6 +187,9 @@ impl TryFrom<&str> for Setting {
181187
"chat.enableTodoList" => Ok(Self::EnabledTodoList),
182188
"chat.enableCheckpoint" => Ok(Self::EnabledCheckpoint),
183189
"chat.enableContextUsageIndicator" => Ok(Self::EnabledContextUsageIndicator),
190+
"chat.enableDelegate" => Ok(Self::EnabledDelegate),
191+
"chat.enableAutoSave" => Ok(Self::ChatEnableAutoSave),
192+
"chat.autoSavePath" => Ok(Self::ChatAutoSavePath),
184193
"chat.uiMode" => Ok(Self::UiMode),
185194
_ => Err(DatabaseError::InvalidSetting(value.to_string())),
186195
}

0 commit comments

Comments
 (0)