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