Skip to content

Commit 95b5a29

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 e3cf013 commit 95b5a29

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
@@ -12,6 +12,7 @@ pub mod cli;
1212
mod consts;
1313
pub mod context;
1414
mod conversation;
15+
mod auto_save;
1516
mod input_source;
1617
mod message;
1718
mod parse;
@@ -677,6 +678,7 @@ pub struct ChatSession {
677678
prompt_ack_rx: std::sync::mpsc::Receiver<()>,
678679
/// Additional context to be added to the next user message (e.g., delegate task summaries)
679680
pending_additional_context: Option<String>,
681+
auto_save_manager: auto_save::AutoSaveManager,
680682
}
681683

682684
impl ChatSession {
@@ -814,6 +816,7 @@ impl ChatSession {
814816
wrap,
815817
prompt_ack_rx,
816818
pending_additional_context: None,
819+
auto_save_manager: auto_save::AutoSaveManager::new(),
817820
})
818821
}
819822

@@ -1172,6 +1175,11 @@ impl ChatSession {
11721175
self.tool_turn_start_time = None;
11731176
self.reset_user_turn();
11741177

1178+
// Auto-save conversation if enabled
1179+
if let Err(e) = self.auto_save_manager.auto_save_if_enabled(os, &self.conversation).await {
1180+
warn!("Auto-save error: {}", e);
1181+
}
1182+
11751183
self.inner = Some(ChatState::PromptUser {
11761184
skip_printing_tools: false,
11771185
});
@@ -3207,6 +3215,11 @@ impl ChatSession {
32073215
.await;
32083216
}
32093217

3218+
// Auto-save conversation if enabled
3219+
if let Err(e) = self.auto_save_manager.auto_save_if_enabled(os, &self.conversation).await {
3220+
warn!("Auto-save error: {}", e);
3221+
}
3222+
32103223
Ok(ChatState::PromptUser {
32113224
skip_printing_tools: false,
32123225
})

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

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -89,6 +89,10 @@ pub enum Setting {
8989
EnabledCheckpoint,
9090
#[strum(message = "Enable the delegate tool for subagent management (boolean)")]
9191
EnabledDelegate,
92+
#[strum(message = "Enable automatic conversation saving (boolean)")]
93+
ChatEnableAutoSave,
94+
#[strum(message = "Auto-save file path pattern (string)")]
95+
ChatAutoSavePath,
9296
#[strum(message = "Specify UI variant to use (string)")]
9397
UiMode,
9498
}
@@ -132,6 +136,8 @@ impl AsRef<str> for Setting {
132136
Self::EnabledCheckpoint => "chat.enableCheckpoint",
133137
Self::EnabledContextUsageIndicator => "chat.enableContextUsageIndicator",
134138
Self::EnabledDelegate => "chat.enableDelegate",
139+
Self::ChatEnableAutoSave => "chat.enableAutoSave",
140+
Self::ChatAutoSavePath => "chat.autoSavePath",
135141
Self::UiMode => "chat.uiMode",
136142
}
137143
}
@@ -182,6 +188,9 @@ impl TryFrom<&str> for Setting {
182188
"chat.enableTodoList" => Ok(Self::EnabledTodoList),
183189
"chat.enableCheckpoint" => Ok(Self::EnabledCheckpoint),
184190
"chat.enableContextUsageIndicator" => Ok(Self::EnabledContextUsageIndicator),
191+
"chat.enableDelegate" => Ok(Self::EnabledDelegate),
192+
"chat.enableAutoSave" => Ok(Self::ChatEnableAutoSave),
193+
"chat.autoSavePath" => Ok(Self::ChatAutoSavePath),
185194
"chat.uiMode" => Ok(Self::UiMode),
186195
_ => Err(DatabaseError::InvalidSetting(value.to_string())),
187196
}

0 commit comments

Comments
 (0)