1use crate::{assistant_settings::OpenAiModel, MessageId, MessageMetadata};
2use anyhow::{anyhow, Result};
3use collections::HashMap;
4use fs::Fs;
5use futures::StreamExt;
6use regex::Regex;
7use serde::{Deserialize, Serialize};
8use std::{
9 cmp::Reverse,
10 ffi::OsStr,
11 path::{Path, PathBuf},
12 sync::Arc,
13};
14use util::paths::CONVERSATIONS_DIR;
15
16#[derive(Serialize, Deserialize)]
17pub struct SavedMessage {
18 pub id: MessageId,
19 pub start: usize,
20}
21
22#[derive(Serialize, Deserialize)]
23pub struct SavedConversation {
24 pub id: Option<String>,
25 pub zed: String,
26 pub version: String,
27 pub text: String,
28 pub messages: Vec<SavedMessage>,
29 pub message_metadata: HashMap<MessageId, MessageMetadata>,
30 pub summary: String,
31}
32
33impl SavedConversation {
34 pub const VERSION: &'static str = "0.2.0";
35
36 pub async fn load(path: &Path, fs: &dyn Fs) -> Result<Self> {
37 let saved_conversation = fs.load(path).await?;
38 let saved_conversation_json =
39 serde_json::from_str::<serde_json::Value>(&saved_conversation)?;
40 match saved_conversation_json
41 .get("version")
42 .ok_or_else(|| anyhow!("version not found"))?
43 {
44 serde_json::Value::String(version) => match version.as_str() {
45 Self::VERSION => Ok(serde_json::from_value::<Self>(saved_conversation_json)?),
46 "0.1.0" => {
47 let saved_conversation =
48 serde_json::from_value::<SavedConversationV0_1_0>(saved_conversation_json)?;
49 Ok(Self {
50 id: saved_conversation.id,
51 zed: saved_conversation.zed,
52 version: saved_conversation.version,
53 text: saved_conversation.text,
54 messages: saved_conversation.messages,
55 message_metadata: saved_conversation.message_metadata,
56 summary: saved_conversation.summary,
57 })
58 }
59 _ => Err(anyhow!(
60 "unrecognized saved conversation version: {}",
61 version
62 )),
63 },
64 _ => Err(anyhow!("version not found on saved conversation")),
65 }
66 }
67}
68
69#[derive(Serialize, Deserialize)]
70struct SavedConversationV0_1_0 {
71 id: Option<String>,
72 zed: String,
73 version: String,
74 text: String,
75 messages: Vec<SavedMessage>,
76 message_metadata: HashMap<MessageId, MessageMetadata>,
77 summary: String,
78 api_url: Option<String>,
79 model: OpenAiModel,
80}
81
82pub struct SavedConversationMetadata {
83 pub title: String,
84 pub path: PathBuf,
85 pub mtime: chrono::DateTime<chrono::Local>,
86}
87
88impl SavedConversationMetadata {
89 pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
90 fs.create_dir(&CONVERSATIONS_DIR).await?;
91
92 let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
93 let mut conversations = Vec::<SavedConversationMetadata>::new();
94 while let Some(path) = paths.next().await {
95 let path = path?;
96 if path.extension() != Some(OsStr::new("json")) {
97 continue;
98 }
99
100 let pattern = r" - \d+.zed.json$";
101 let re = Regex::new(pattern).unwrap();
102
103 let metadata = fs.metadata(&path).await?;
104 if let Some((file_name, metadata)) = path
105 .file_name()
106 .and_then(|name| name.to_str())
107 .zip(metadata)
108 {
109 // This is used to filter out conversations saved by the new assistant.
110 if !re.is_match(file_name) {
111 continue;
112 }
113
114 let title = re.replace(file_name, "");
115 conversations.push(Self {
116 title: title.into_owned(),
117 path,
118 mtime: metadata.mtime.into(),
119 });
120 }
121 }
122 conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
123
124 Ok(conversations)
125 }
126}