diff --git a/Cargo.lock b/Cargo.lock index ed7d2208de..8e0b11d5ca 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -377,6 +377,7 @@ dependencies = [ "anyhow", "assets", "assistant_tooling", + "chrono", "client", "collections", "editor", @@ -395,6 +396,7 @@ dependencies = [ "picker", "project", "rand 0.8.5", + "regex", "release_channel", "rich_text", "schemars", diff --git a/crates/assistant2/Cargo.toml b/crates/assistant2/Cargo.toml index 51602c4162..27f1ebcbcc 100644 --- a/crates/assistant2/Cargo.toml +++ b/crates/assistant2/Cargo.toml @@ -19,6 +19,7 @@ stories = ["dep:story"] anyhow.workspace = true assistant_tooling.workspace = true client.workspace = true +chrono.workspace = true collections.workspace = true editor.workspace = true feature_flags.workspace = true @@ -32,6 +33,7 @@ nanoid.workspace = true open_ai.workspace = true picker.workspace = true project.workspace = true +regex.workspace = true rich_text.workspace = true schemars.workspace = true semantic_index.workspace = true diff --git a/crates/assistant2/src/saved_conversation.rs b/crates/assistant2/src/saved_conversation.rs index 1434b777f5..a46f8a54c9 100644 --- a/crates/assistant2/src/saved_conversation.rs +++ b/crates/assistant2/src/saved_conversation.rs @@ -1,6 +1,16 @@ +use std::cmp::Reverse; +use std::ffi::OsStr; +use std::path::PathBuf; +use std::sync::Arc; + +use anyhow::Result; use assistant_tooling::{SavedToolFunctionCall, SavedUserAttachment}; +use fs::Fs; +use futures::StreamExt; use gpui::SharedString; +use regex::Regex; use serde::{Deserialize, Serialize}; +use util::paths::CONVERSATIONS_DIR; use crate::MessageId; @@ -33,25 +43,48 @@ pub struct SavedAssistantMessagePart { pub tool_calls: Vec, } -/// Returns a list of placeholder conversations for mocking the UI. -/// -/// Once we have real saved conversations to pull from we can use those instead. -pub fn placeholder_conversations() -> Vec { - vec![ - SavedConversation { - version: "0.3.0".to_string(), - title: "How to get a list of exported functions in an Erlang module".to_string(), - messages: vec![], - }, - SavedConversation { - version: "0.3.0".to_string(), - title: "7 wonders of the ancient world".to_string(), - messages: vec![], - }, - SavedConversation { - version: "0.3.0".to_string(), - title: "Size difference between u8 and a reference to u8 in Rust".to_string(), - messages: vec![], - }, - ] +pub struct SavedConversationMetadata { + pub title: String, + pub path: PathBuf, + pub mtime: chrono::DateTime, +} + +impl SavedConversationMetadata { + pub async fn list(fs: Arc) -> Result> { + fs.create_dir(&CONVERSATIONS_DIR).await?; + + let mut paths = fs.read_dir(&CONVERSATIONS_DIR).await?; + let mut conversations = Vec::new(); + while let Some(path) = paths.next().await { + let path = path?; + if path.extension() != Some(OsStr::new("json")) { + continue; + } + + let pattern = r" - \d+.zed.\d.\d.\d.json$"; + let re = Regex::new(pattern).unwrap(); + + let metadata = fs.metadata(&path).await?; + if let Some((file_name, metadata)) = path + .file_name() + .and_then(|name| name.to_str()) + .zip(metadata) + { + // This is used to filter out conversations saved by the old assistant. + if !re.is_match(file_name) { + continue; + } + + let title = re.replace(file_name, ""); + conversations.push(Self { + title: title.into_owned(), + path, + mtime: metadata.mtime.into(), + }); + } + } + conversations.sort_unstable_by_key(|conversation| Reverse(conversation.mtime)); + + Ok(conversations) + } } diff --git a/crates/assistant2/src/saved_conversation_picker.rs b/crates/assistant2/src/saved_conversation_picker.rs index 3c16fdac69..17962cb1f3 100644 --- a/crates/assistant2/src/saved_conversation_picker.rs +++ b/crates/assistant2/src/saved_conversation_picker.rs @@ -7,7 +7,7 @@ use ui::{prelude::*, HighlightedLabel, ListItem, ListItemSpacing}; use util::ResultExt; use workspace::{ModalView, Workspace}; -use crate::saved_conversation::{self, SavedConversation}; +use crate::saved_conversation::SavedConversationMetadata; use crate::ToggleSavedConversations; pub struct SavedConversationPicker { @@ -27,10 +27,26 @@ impl FocusableView for SavedConversationPicker { impl SavedConversationPicker { pub fn register(workspace: &mut Workspace, _cx: &mut ViewContext) { workspace.register_action(|workspace, _: &ToggleSavedConversations, cx| { - workspace.toggle_modal(cx, move |cx| { - let delegate = SavedConversationPickerDelegate::new(cx.view().downgrade()); - Self::new(delegate, cx) - }); + let fs = workspace.project().read(cx).fs().clone(); + + cx.spawn(|workspace, mut cx| async move { + let saved_conversations = SavedConversationMetadata::list(fs).await?; + + cx.update(|cx| { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, move |cx| { + let delegate = SavedConversationPickerDelegate::new( + cx.view().downgrade(), + saved_conversations, + ); + Self::new(delegate, cx) + }); + }) + })??; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); }); } @@ -48,14 +64,16 @@ impl Render for SavedConversationPicker { pub struct SavedConversationPickerDelegate { view: WeakView, - saved_conversations: Vec, + saved_conversations: Vec, selected_index: usize, matches: Vec, } impl SavedConversationPickerDelegate { - pub fn new(weak_view: WeakView) -> Self { - let saved_conversations = saved_conversation::placeholder_conversations(); + pub fn new( + weak_view: WeakView, + saved_conversations: Vec, + ) -> Self { let matches = saved_conversations .iter() .map(|conversation| StringMatch {