diff --git a/assets/keymaps/default.json b/assets/keymaps/default.json index a642697a3765e81c62b2c7808847c9be29535a88..cce8b07d17491dc99e377a993966a9490c45380d 100644 --- a/assets/keymaps/default.json +++ b/assets/keymaps/default.json @@ -200,6 +200,7 @@ "context": "AssistantEditor > Editor", "bindings": { "cmd-enter": "assistant::Assist", + "cmd-s": "workspace::Save", "cmd->": "assistant::QuoteSelection", "shift-enter": "assistant::Split", "ctrl-r": "assistant::CycleMessageRole" diff --git a/crates/ai/src/ai.rs b/crates/ai/src/ai.rs index 40224b3229de1665e3fac89be0d035154e2cf67f..a46076831f441ac143fcaa494533425a9b6fd4af 100644 --- a/crates/ai/src/ai.rs +++ b/crates/ai/src/ai.rs @@ -14,6 +14,13 @@ struct OpenAIRequest { stream: bool, } +#[derive(Serialize, Deserialize)] +struct SavedConversation { + zed: String, + version: String, + messages: Vec, +} + #[derive(Serialize, Deserialize, Debug, Eq, PartialEq)] struct RequestMessage { role: Role, diff --git a/crates/ai/src/assistant.rs b/crates/ai/src/assistant.rs index 853f7262d33f07a1636b39add08ebd9ffbad239b..a84666d3bf367611d14600f4023584f92882a226 100644 --- a/crates/ai/src/assistant.rs +++ b/crates/ai/src/assistant.rs @@ -1,6 +1,6 @@ use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings}, - OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, + OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, SavedConversation, }; use anyhow::{anyhow, Result}; use chrono::{DateTime, Local}; @@ -29,11 +29,14 @@ use std::{ borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc, time::Duration, }; -use util::{channel::ReleaseChannel, post_inc, truncate_and_trailoff, ResultExt, TryFutureExt}; +use util::{ + channel::ReleaseChannel, paths::CONVERSATIONS_DIR, post_inc, truncate_and_trailoff, ResultExt, + TryFutureExt, +}; use workspace::{ dock::{DockPosition, Panel}, item::Item, - pane, Pane, Workspace, + pane, Pane, Save, Workspace, }; const OPENAI_API_URL: &'static str = "https://api.openai.com/v1"; @@ -47,7 +50,7 @@ actions!( CycleMessageRole, QuoteSelection, ToggleFocus, - ResetKey + ResetKey, ] ); @@ -70,6 +73,7 @@ pub fn init(cx: &mut AppContext) { ); cx.add_action(AssistantEditor::assist); cx.capture_action(AssistantEditor::cancel_last_assist); + cx.capture_action(AssistantEditor::save); cx.add_action(AssistantEditor::quote_selection); cx.capture_action(AssistantEditor::copy); cx.capture_action(AssistantEditor::split); @@ -215,8 +219,14 @@ impl AssistantPanel { fn add_context(&mut self, cx: &mut ViewContext) { let focus = self.has_focus(cx); - let editor = cx - .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx)); + let editor = cx.add_view(|cx| { + AssistantEditor::new( + self.api_key.clone(), + self.languages.clone(), + self.fs.clone(), + cx, + ) + }); self.subscriptions .push(cx.subscribe(&editor, Self::handle_assistant_editor_event)); self.pane.update(cx, |pane, cx| { @@ -922,6 +932,33 @@ impl Assistant { None }) } + + fn save(&self, fs: Arc, cx: &mut ModelContext) -> Task> { + let conversation = SavedConversation { + zed: "conversation".into(), + version: "0.1".into(), + messages: self.open_ai_request_messages(cx), + }; + + let mut path = CONVERSATIONS_DIR.join(self.summary.as_deref().unwrap_or("conversation-1")); + + cx.background().spawn(async move { + while fs.is_file(&path).await { + let file_name = path.file_name().ok_or_else(|| anyhow!("no filename"))?; + let file_name = file_name.to_string_lossy(); + + if let Some((prefix, suffix)) = file_name.rsplit_once('-') { + let new_version = suffix.parse::().ok().unwrap_or(1) + 1; + path.set_file_name(format!("{}-{}", prefix, new_version)); + }; + } + + fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?; + fs.atomic_write(path, serde_json::to_string(&conversation).unwrap()) + .await?; + Ok(()) + }) + } } struct PendingCompletion { @@ -941,6 +978,7 @@ struct ScrollPosition { struct AssistantEditor { assistant: ModelHandle, + fs: Arc, editor: ViewHandle, blocks: HashSet, scroll_position: Option, @@ -951,6 +989,7 @@ impl AssistantEditor { fn new( api_key: Rc>>, language_registry: Arc, + fs: Arc, cx: &mut ViewContext, ) -> Self { let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx)); @@ -972,6 +1011,7 @@ impl AssistantEditor { editor, blocks: Default::default(), scroll_position: None, + fs, _subscriptions, }; this.update_message_headers(cx); @@ -1299,6 +1339,12 @@ impl AssistantEditor { }); } + fn save(&mut self, _: &Save, cx: &mut ViewContext) { + self.assistant.update(cx, |assistant, cx| { + assistant.save(self.fs.clone(), cx).detach_and_log_err(cx); + }); + } + fn cycle_model(&mut self, cx: &mut ViewContext) { self.assistant.update(cx, |assistant, cx| { let new_model = match assistant.model.as_str() { diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index e3397a1557cfeed520e4c56fa38c40e4539d71a9..7ef55a991822a52ba18e3dc45184f9881a38dc74 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -5,6 +5,7 @@ use serde::{Deserialize, Serialize}; lazy_static::lazy_static! { pub static ref HOME: PathBuf = dirs::home_dir().expect("failed to determine home directory"); pub static ref CONFIG_DIR: PathBuf = HOME.join(".config").join("zed"); + pub static ref CONVERSATIONS_DIR: PathBuf = HOME.join(".config/zed/conversations"); pub static ref LOGS_DIR: PathBuf = HOME.join("Library/Logs/Zed"); pub static ref SUPPORT_DIR: PathBuf = HOME.join("Library/Application Support/Zed"); pub static ref LANGUAGES_DIR: PathBuf = HOME.join("Library/Application Support/Zed/languages");