saved_conversation.rs

 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}