mermaid_cli/session/
conversation.rs1use crate::models::{ChatMessage, MessageRole};
2use anyhow::Result;
3use chrono::{DateTime, Local};
4use serde::{Deserialize, Serialize};
5use std::fs;
6use std::path::{Path, PathBuf};
7
8#[derive(Debug, Clone, Serialize, Deserialize)]
10pub struct ConversationHistory {
11 pub id: String,
12 pub title: String,
13 pub messages: Vec<ChatMessage>,
14 pub model_name: String,
15 pub project_path: String,
16 pub created_at: DateTime<Local>,
17 pub updated_at: DateTime<Local>,
18 pub total_tokens: Option<usize>,
19 #[serde(default)]
21 pub input_history: Vec<String>,
22}
23
24impl ConversationHistory {
25 pub fn new(project_path: String, model_name: String) -> Self {
27 let now = Local::now();
28 let id = format!("{}", now.format("%Y%m%d_%H%M%S"));
29 Self {
30 id: id.clone(),
31 title: format!("Session {}", now.format("%Y-%m-%d %H:%M")),
32 messages: Vec::new(),
33 model_name,
34 project_path,
35 created_at: now,
36 updated_at: now,
37 total_tokens: None,
38 input_history: Vec::new(),
39 }
40 }
41
42 pub fn add_messages(&mut self, messages: &[ChatMessage]) {
44 self.messages.extend_from_slice(messages);
45 self.updated_at = Local::now();
46 self.update_title();
47 }
48
49 pub fn add_to_input_history(&mut self, input: String) {
51 if input.trim().is_empty() {
53 return;
54 }
55
56 if let Some(last) = self.input_history.last() {
58 if last == &input {
59 return;
60 }
61 }
62
63 if self.input_history.len() >= 100 {
65 self.input_history.remove(0);
66 }
67
68 self.input_history.push(input);
69 }
70
71 fn update_title(&mut self) {
73 if let Some(first_user_msg) = self.messages.iter().find(|m| m.role == MessageRole::User) {
74 let preview = if first_user_msg.content.len() > 60 {
76 format!("{}...", &first_user_msg.content[..60])
77 } else {
78 first_user_msg.content.clone()
79 };
80 self.title = preview;
81 }
82 }
83
84 pub fn summary(&self) -> String {
86 let message_count = self.messages.len();
87 let duration = self.updated_at.signed_duration_since(self.created_at);
88 let hours = duration.num_hours();
89 let minutes = duration.num_minutes() % 60;
90
91 format!(
92 "{} | {} messages | {}h {}m | {}",
93 self.updated_at.format("%Y-%m-%d %H:%M"),
94 message_count,
95 hours,
96 minutes,
97 self.title
98 )
99 }
100}
101
102pub struct ConversationManager {
104 #[allow(dead_code)]
105 project_dir: PathBuf,
106 conversations_dir: PathBuf,
107}
108
109impl ConversationManager {
110 pub fn new(project_dir: impl AsRef<Path>) -> Result<Self> {
112 let project_dir = project_dir.as_ref().to_path_buf();
113 let conversations_dir = project_dir.join(".mermaid").join("conversations");
114
115 fs::create_dir_all(&conversations_dir)?;
117
118 Ok(Self {
119 project_dir,
120 conversations_dir,
121 })
122 }
123
124 pub fn save_conversation(&self, conversation: &ConversationHistory) -> Result<()> {
126 let filename = format!("{}.json", conversation.id);
127 let path = self.conversations_dir.join(filename);
128
129 let json = serde_json::to_string_pretty(conversation)?;
130 fs::write(path, json)?;
131
132 Ok(())
133 }
134
135 pub fn load_conversation(&self, id: &str) -> Result<ConversationHistory> {
137 let filename = format!("{}.json", id);
138 let path = self.conversations_dir.join(filename);
139
140 let json = fs::read_to_string(path)?;
141 let conversation: ConversationHistory = serde_json::from_str(&json)?;
142
143 Ok(conversation)
144 }
145
146 pub fn load_last_conversation(&self) -> Result<Option<ConversationHistory>> {
148 let conversations = self.list_conversations()?;
149
150 if conversations.is_empty() {
151 return Ok(None);
152 }
153
154 Ok(conversations.into_iter().next())
156 }
157
158 pub fn list_conversations(&self) -> Result<Vec<ConversationHistory>> {
160 let mut conversations = Vec::new();
161
162 if let Ok(entries) = fs::read_dir(&self.conversations_dir) {
164 for entry in entries.flatten() {
165 if let Some(ext) = entry.path().extension() {
166 if ext == "json" {
167 if let Ok(json) = fs::read_to_string(entry.path()) {
168 if let Ok(conv) = serde_json::from_str::<ConversationHistory>(&json) {
169 conversations.push(conv);
170 }
171 }
172 }
173 }
174 }
175 }
176
177 conversations.sort_by(|a, b| b.updated_at.cmp(&a.updated_at));
179
180 Ok(conversations)
181 }
182
183 pub fn delete_conversation(&self, id: &str) -> Result<()> {
185 let filename = format!("{}.json", id);
186 let path = self.conversations_dir.join(filename);
187
188 if path.exists() {
189 fs::remove_file(path)?;
190 }
191
192 Ok(())
193 }
194
195 pub fn conversations_dir(&self) -> &Path {
197 &self.conversations_dir
198 }
199}