1use std::cmp::Reverse;
2use std::ffi::OsStr;
3use std::path::PathBuf;
4use std::sync::Arc;
5
6use anyhow::Result;
7use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment};
8use fs::Fs;
9use futures::StreamExt;
10use gpui::SharedString;
11use regex::Regex;
12use serde::{Deserialize, Serialize};
13use util::paths::CONVERSATIONS_DIR;
14
15use crate::MessageId;
16
17#[derive(Serialize, Deserialize)]
18pub struct SavedConversation {
19 /// The schema version of the conversation.
20 pub version: String,
21 /// The title of the conversation, generated by the Assistant.
22 pub title: String,
23 pub messages: Vec<SavedChatMessage>,
24}
25
26#[derive(Serialize, Deserialize)]
27pub enum SavedChatMessage {
28 User {
29 id: MessageId,
30 body: String,
31 attachments: Vec<SavedUserAttachment>,
32 },
33 Assistant {
34 id: MessageId,
35 messages: Vec<SavedAssistantMessagePart>,
36 error: Option<SharedString>,
37 },
38}
39
40#[derive(Serialize, Deserialize)]
41pub struct SavedAssistantMessagePart {
42 pub body: SharedString,
43 pub tool_calls: Vec<SavedToolFunctionCall>,
44}
45
46pub struct SavedConversationMetadata {
47 pub title: String,
48 pub path: PathBuf,
49 pub mtime: chrono::DateTime<chrono::Local>,
50}
51
52impl SavedConversationMetadata {
53 pub async fn list(fs: Arc<dyn Fs>) -> Result<Vec<Self>> {
54 fs.create_dir(&CONVERSATIONS_DIR).await?;
55
56 let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?;
57 let mut conversations = Vec::new();
58 while let Some(path) = paths.next().await {
59 let path = path?;
60 if path.extension() != Some(OsStr::new("json")) {
61 continue;
62 }
63
64 let pattern = r" - \d+.zed.\d.\d.\d.json$";
65 let re = Regex::new(pattern).unwrap();
66
67 let metadata = fs.metadata(&path).await?;
68 if let Some((file_name, metadata)) = path
69 .file_name()
70 .and_then(|name| name.to_str())
71 .zip(metadata)
72 {
73 // This is used to filter out conversations saved by the old assistant.
74 if !re.is_match(file_name) {
75 continue;
76 }
77
78 let title = re.replace(file_name, "");
79 conversations.push(Self {
80 title: title.into_owned(),
81 path,
82 mtime: metadata.mtime.into(),
83 });
84 }
85 }
86 conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime));
87
88 Ok(conversations)
89 }
90}