From f8672289fc64145ad0170299be474eefa06ed8a2 Mon Sep 17 00:00:00 2001 From: Nate Butler Date: Thu, 16 May 2024 16:55:54 -0400 Subject: [PATCH] Add prompt library (#11910) This PR adds a Prompt Library to Zed, powering custom prompts and any default prompts we want to package with the assistant. These are useful for: - Creating a "default prompt" - a super prompt that includes a collection of things you want the assistant to know in every conversation. - Adding single prompts to your current context to help guide the assistant's responses. - (In the future) dynamically adding certain prompts to the assistant based on the current context, such as the presence of Rust code or a specific async runtime you want to work with. These will also be useful for populating the assistant actions typeahead we plan to build in the near future. ## Prompt Library The prompt library is a registry of prompts. Initially by default when opening the assistant, the prompt manager will load any custom prompts present in your `~/.config/zed/prompts` directory. Checked prompts are included in your "default prompt", which can be inserted into the assitant by running `assistant: insert default prompt` or clicking the `Insert Default Prompt` button in the assistant panel's more menu. When the app starts, no prompts are set to default. You can add prompts to the default by checking them in the Prompt Library. I plan to improve this UX in the future, allowing your default prompts to be remembered, and allowing creating, editing and exporting prompts from the Library. ### Creating a custom prompt Prompts have a simple format: ```json { // ~/.config/zed/prompts/no-comments.json "title": "No comments in code", "version": "1.0", "author": "Nate Butler ", "languages": ["*"], "prompt": "Do not add inline or doc comments to any returned code. Avoid removing existing comments unless they are no longer accurate due to changes in the code." } ``` Ensure you properly escape your prompt string when creating a new prompt file. Example: ```json { // ... "prompt": "This project using the gpui crate as it's UI framework for building UI in Rust. When working in Rust files with gpui components, import it's dependencies using `use gpui::{*, prelude::*}`.\n\nWhen a struct has a `#[derive(IntoElement)]` attribute, it is a UI component that must implement `RenderOnce`. Example:\n\n```rust\n#[derive(IntoElement)]\nstruct MyComponent {\n id: ElementId,\n}\n\nimpl MyComponent {\n pub fn new(id: impl Into) -> Self {\n Self { id.into() }\n }\n}\n\nimpl RenderOnce for MyComponent {\n fn render(self, cx: &mut WindowContext) -> impl IntoElement {\n div().id(self.id.clone()).child(text(\"Hello, world!\"))\n }\n}\n```" } ``` Release Notes: - N/A --------- Co-authored-by: Marshall Bowers --- assets/icons/library.svg | 1 + crates/assistant/src/assistant.rs | 2 + crates/assistant/src/assistant_panel.rs | 135 +++++-- crates/assistant/src/prompt_library.rs | 454 ++++++++++++++++++++++++ crates/ui/src/components/icon.rs | 48 +-- crates/ui/src/components/modal.rs | 5 +- crates/util/src/paths.rs | 5 + docs/src/assistant-panel.md | 44 +++ 8 files changed, 638 insertions(+), 56 deletions(-) create mode 100644 assets/icons/library.svg create mode 100644 crates/assistant/src/prompt_library.rs diff --git a/assets/icons/library.svg b/assets/icons/library.svg new file mode 100644 index 0000000000000000000000000000000000000000..95f8c710c8673ca78fcb9e72b0c8ab4be6d8ffbc --- /dev/null +++ b/assets/icons/library.svg @@ -0,0 +1 @@ + diff --git a/crates/assistant/src/assistant.rs b/crates/assistant/src/assistant.rs index df75ab631489296ec3b79fa27c09fb21c8a6c672..cf7242e53a6dad9407aea8bdebf4f3a1b3e9fba6 100644 --- a/crates/assistant/src/assistant.rs +++ b/crates/assistant/src/assistant.rs @@ -3,6 +3,7 @@ pub mod assistant_panel; pub mod assistant_settings; mod codegen; mod completion_provider; +mod prompt_library; mod prompts; mod saved_conversation; mod streaming_diff; @@ -31,6 +32,7 @@ actions!( ToggleFocus, ResetKey, InlineAssist, + InsertActivePrompt, ToggleIncludeConversation, ToggleHistory, ] diff --git a/crates/assistant/src/assistant_panel.rs b/crates/assistant/src/assistant_panel.rs index eb463580b8e65aa0e1c7c8fd44412234e27e7c89..8d8b5dbdb39a8ee41ccefae348471f92d58ec69a 100644 --- a/crates/assistant/src/assistant_panel.rs +++ b/crates/assistant/src/assistant_panel.rs @@ -1,7 +1,9 @@ use crate::ambient_context::{AmbientContext, ContextUpdated, RecentBuffer}; +use crate::InsertActivePrompt; use crate::{ assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel}, codegen::{self, Codegen, CodegenKind}, + prompt_library::{PromptLibrary, PromptManager}, prompts::generate_content_prompt, Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus, @@ -74,6 +76,7 @@ pub fn init(cx: &mut AppContext) { }) .register_action(AssistantPanel::inline_assist) .register_action(AssistantPanel::cancel_last_inline_assist) + .register_action(ConversationEditor::insert_active_prompt) .register_action(ConversationEditor::quote_selection); }, ) @@ -92,6 +95,7 @@ pub struct AssistantPanel { focus_handle: FocusHandle, toolbar: View, languages: Arc, + prompt_library: Arc, fs: Arc, telemetry: Arc, _subscriptions: Vec, @@ -124,6 +128,13 @@ impl AssistantPanel { .log_err() .unwrap_or_default(); + let prompt_library = Arc::new( + PromptLibrary::init(fs.clone()) + .await + .log_err() + .unwrap_or_default(), + ); + // TODO: deserialize state. let workspace_handle = workspace.clone(); workspace.update(&mut cx, |workspace, cx| { @@ -186,6 +197,7 @@ impl AssistantPanel { focus_handle, toolbar, languages: workspace.app_state().languages.clone(), + prompt_library, fs: workspace.app_state().fs.clone(), telemetry: workspace.client().telemetry().clone(), width: None, @@ -1005,6 +1017,20 @@ impl AssistantPanel { .ok(); } }) + .entry("Insert Active Prompt", None, { + let workspace = workspace.clone(); + move |cx| { + workspace + .update(cx, |workspace, cx| { + ConversationEditor::insert_active_prompt( + workspace, + &Default::default(), + cx, + ) + }) + .ok(); + } + }) }) .into() }) @@ -1083,6 +1109,14 @@ impl AssistantPanel { }) } + fn show_prompt_manager(&mut self, cx: &mut ViewContext) { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + workspace.toggle_modal(cx, |cx| PromptManager::new(self.prompt_library.clone(), cx)) + }) + } + } + fn is_authenticated(&mut self, cx: &mut ViewContext) -> bool { CompletionProvider::global(cx).is_authenticated() } @@ -1092,39 +1126,48 @@ impl AssistantPanel { } fn render_signed_in(&mut self, cx: &mut ViewContext) -> impl IntoElement { - let header = TabBar::new("assistant_header") - .start_child(h_flex().gap_1().child(self.render_popover_button(cx))) - .children(self.active_conversation_editor().map(|editor| { - h_flex() - .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS)) - .flex_1() - .px_2() - .child(Label::new(editor.read(cx).title(cx)).into_element()) - })) - .end_child( - h_flex() - .gap_2() - .when_some(self.active_conversation_editor(), |this, editor| { - let conversation = editor.read(cx).conversation.clone(); - this.child( + let header = + TabBar::new("assistant_header") + .start_child(h_flex().gap_1().child(self.render_popover_button(cx))) + .children(self.active_conversation_editor().map(|editor| { + h_flex() + .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS)) + .flex_1() + .px_2() + .child(Label::new(editor.read(cx).title(cx)).into_element()) + })) + .end_child( + h_flex() + .gap_2() + .when_some(self.active_conversation_editor(), |this, editor| { + let conversation = editor.read(cx).conversation.clone(); + this.child( + h_flex() + .gap_1() + .child(self.render_model(&conversation, cx)) + .children(self.render_remaining_tokens(&conversation, cx)), + ) + .child( + ui::Divider::vertical() + .inset() + .color(ui::DividerColor::Border), + ) + }) + .child( h_flex() .gap_1() - .child(self.render_model(&conversation, cx)) - .children(self.render_remaining_tokens(&conversation, cx)), - ) - .child( - ui::Divider::vertical() - .inset() - .color(ui::DividerColor::Border), - ) - }) - .child( - h_flex() - .gap_1() - .child(self.render_inject_context_menu(cx)) - .child(Self::render_assist_button(cx)), - ), - ); + .child(self.render_inject_context_menu(cx)) + .child( + IconButton::new("show_prompt_manager", IconName::Library) + .icon_size(IconSize::Small) + .on_click(cx.listener(|this, _event, cx| { + this.show_prompt_manager(cx) + })) + .tooltip(|cx| Tooltip::text("Prompt Library…", cx)), + ) + .child(Self::render_assist_button(cx)), + ), + ); let contents = if self.active_conversation_editor().is_some() { let mut registrar = DivRegistrar::new( @@ -2618,6 +2661,36 @@ impl ConversationEditor { } } + fn insert_active_prompt( + workspace: &mut Workspace, + _: &InsertActivePrompt, + cx: &mut ViewContext, + ) { + let Some(panel) = workspace.panel::(cx) else { + return; + }; + + if !panel.focus_handle(cx).contains_focused(cx) { + workspace.toggle_panel_focus::(cx); + } + + if let Some(default_prompt) = panel.read(cx).prompt_library.clone().default_prompt() { + panel.update(cx, |panel, cx| { + if let Some(conversation) = panel + .active_conversation_editor() + .cloned() + .or_else(|| panel.new_conversation(cx)) + { + conversation.update(cx, |conversation, cx| { + conversation + .editor + .update(cx, |editor, cx| editor.insert(&default_prompt, cx)) + }); + }; + }); + }; + } + fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext) { let editor = self.editor.read(cx); let conversation = self.conversation.read(cx); diff --git a/crates/assistant/src/prompt_library.rs b/crates/assistant/src/prompt_library.rs new file mode 100644 index 0000000000000000000000000000000000000000..3490b134782f8973c38d6e1339a9412587f590f1 --- /dev/null +++ b/crates/assistant/src/prompt_library.rs @@ -0,0 +1,454 @@ +use fs::Fs; +use futures::StreamExt; +use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render}; +use parking_lot::RwLock; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::sync::Arc; +use ui::{prelude::*, Checkbox, ModalHeader}; +use util::{paths::PROMPTS_DIR, ResultExt}; +use workspace::ModalView; + +pub struct PromptLibraryState { + /// The default prompt all assistant contexts will start with + _system_prompt: String, + /// All [UserPrompt]s loaded into the library + prompts: HashMap, + /// Prompts included in the default prompt + default_prompts: Vec, + /// Prompts that have a pending update that hasn't been applied yet + _updateable_prompts: Vec, + /// Prompts that have been changed since they were loaded + /// and can be reverted to their original state + _revertable_prompts: Vec, + version: usize, +} + +pub struct PromptLibrary { + state: RwLock, +} + +impl Default for PromptLibrary { + fn default() -> Self { + Self::new() + } +} + +impl PromptLibrary { + fn new() -> Self { + Self { + state: RwLock::new(PromptLibraryState { + _system_prompt: String::new(), + prompts: HashMap::new(), + default_prompts: Vec::new(), + _updateable_prompts: Vec::new(), + _revertable_prompts: Vec::new(), + version: 0, + }), + } + } + + pub async fn init(fs: Arc) -> anyhow::Result { + let prompt_library = PromptLibrary::new(); + prompt_library.load_prompts(fs)?; + Ok(prompt_library) + } + + fn load_prompts(&self, fs: Arc) -> anyhow::Result<()> { + let prompts = futures::executor::block_on(UserPrompt::list(fs))?; + let prompts_with_ids = prompts + .clone() + .into_iter() + .map(|prompt| { + let id = uuid::Uuid::new_v4().to_string(); + (id, prompt) + }) + .collect::>(); + let mut state = self.state.write(); + state.prompts.extend(prompts_with_ids); + state.version += 1; + + Ok(()) + } + + pub fn default_prompt(&self) -> Option { + let state = self.state.read(); + + if state.default_prompts.is_empty() { + None + } else { + Some(self.join_default_prompts()) + } + } + + pub fn add_prompt_to_default(&self, prompt_id: String) -> anyhow::Result<()> { + let mut state = self.state.write(); + + if !state.default_prompts.contains(&prompt_id) && state.prompts.contains_key(&prompt_id) { + state.default_prompts.push(prompt_id); + state.version += 1; + } + + Ok(()) + } + + pub fn remove_prompt_from_default(&self, prompt_id: String) -> anyhow::Result<()> { + let mut state = self.state.write(); + + state.default_prompts.retain(|id| id != &prompt_id); + state.version += 1; + Ok(()) + } + + fn join_default_prompts(&self) -> String { + let state = self.state.read(); + let active_prompt_ids = state.default_prompts.to_vec(); + + active_prompt_ids + .iter() + .filter_map(|id| state.prompts.get(id).map(|p| p.prompt.clone())) + .collect::>() + .join("\n\n---\n\n") + } + + #[allow(unused)] + pub fn prompts(&self) -> Vec { + let state = self.state.read(); + state.prompts.values().cloned().collect() + } + + pub fn prompts_with_ids(&self) -> Vec<(String, UserPrompt)> { + let state = self.state.read(); + state + .prompts + .iter() + .map(|(id, prompt)| (id.clone(), prompt.clone())) + .collect() + } + + pub fn _default_prompts(&self) -> Vec { + let state = self.state.read(); + state + .default_prompts + .iter() + .filter_map(|id| state.prompts.get(id).cloned()) + .collect() + } + + pub fn default_prompt_ids(&self) -> Vec { + let state = self.state.read(); + state.default_prompts.clone() + } +} + +/// A custom prompt that can be loaded into the prompt library +/// +/// Example: +/// +/// ```json +/// { +/// "title": "Foo", +/// "version": "1.0", +/// "author": "Jane Kim ", +/// "languages": ["*"], // or ["rust", "python", "javascript"] etc... +/// "prompt": "bar" +/// } +#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)] +pub struct UserPrompt { + version: String, + title: String, + author: String, + languages: Vec, + prompt: String, +} + +impl UserPrompt { + async fn list(fs: Arc) -> anyhow::Result> { + fs.create_dir(&PROMPTS_DIR).await?; + + let mut paths = fs.read_dir(&PROMPTS_DIR).await?; + let mut prompts = Vec::new(); + + while let Some(path_result) = paths.next().await { + let path = match path_result { + Ok(p) => p, + Err(e) => { + eprintln!("Error reading path: {:?}", e); + continue; + } + }; + + if path.extension() == Some(std::ffi::OsStr::new("json")) { + match fs.load(&path).await { + Ok(content) => { + let user_prompt: UserPrompt = + serde_json::from_str(&content).map_err(|e| { + anyhow::anyhow!("Failed to deserialize UserPrompt: {}", e) + })?; + + prompts.push(user_prompt); + } + Err(e) => eprintln!("Failed to load file {}: {}", path.display(), e), + } + } + } + + Ok(prompts) + } +} + +pub struct PromptManager { + focus_handle: FocusHandle, + prompt_library: Arc, + active_prompt: Option, +} + +impl PromptManager { + pub fn new(prompt_library: Arc, cx: &mut WindowContext) -> Self { + let focus_handle = cx.focus_handle(); + Self { + focus_handle, + prompt_library, + active_prompt: None, + } + } + + pub fn set_active_prompt(&mut self, prompt_id: Option) { + self.active_prompt = prompt_id; + } + + fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext) { + cx.emit(DismissEvent); + } +} + +impl Render for PromptManager { + fn render(&mut self, cx: &mut ViewContext) -> impl IntoElement { + let prompt_library = self.prompt_library.clone(); + let prompts = prompt_library + .clone() + .prompts_with_ids() + .clone() + .into_iter() + .collect::>(); + + let active_prompt = self.active_prompt.as_ref().and_then(|id| { + prompt_library + .prompts_with_ids() + .iter() + .find(|(prompt_id, _)| prompt_id == id) + .map(|(_, prompt)| prompt.clone()) + }); + + v_flex() + .key_context("PromptManager") + .track_focus(&self.focus_handle) + .on_action(cx.listener(Self::dismiss)) + .elevation_3(cx) + .size_full() + .flex_none() + .w(rems(54.)) + .h(rems(40.)) + .overflow_hidden() + .child( + ModalHeader::new("prompt-manager-header") + .child(Headline::new("Prompt Library").size(HeadlineSize::Small)) + .show_dismiss_button(true), + ) + .child( + h_flex() + .flex_grow() + .overflow_hidden() + .border_t_1() + .border_color(cx.theme().colors().border) + .child( + div() + .id("prompt-preview") + .overflow_y_scroll() + .h_full() + .min_w_64() + .max_w_1_2() + .child( + v_flex() + .justify_start() + .py(Spacing::Medium.rems(cx)) + .px(Spacing::Large.rems(cx)) + .bg(cx.theme().colors().surface_background) + .when_else( + !prompts.is_empty(), + |with_items| { + with_items.children(prompts.into_iter().map( + |(id, prompt)| { + let prompt_library = prompt_library.clone(); + let prompt = prompt.clone(); + let prompt_id = id.clone(); + let shared_string_id: SharedString = + id.clone().into(); + + let default_prompt_ids = + prompt_library.clone().default_prompt_ids(); + let is_default = + default_prompt_ids.contains(&id); + // We'll use this for conditionally enabled prompts + // like those loaded only for certain languages + let is_conditional = false; + let selection = + match (is_default, is_conditional) { + (_, true) => Selection::Indeterminate, + (true, _) => Selection::Selected, + (false, _) => Selection::Unselected, + }; + + v_flex() + .id(ElementId::Name( + format!("prompt-{}", shared_string_id) + .into(), + )) + .p(Spacing::Small.rems(cx)) + + .on_click(cx.listener({ + let prompt_id = prompt_id.clone(); + move |this, _event, _cx| { + this.set_active_prompt(Some( + prompt_id.clone(), + )); + } + })) + .child( + h_flex() + .justify_between() + .child( + h_flex() + .gap(Spacing::Large.rems(cx)) + .child( + Checkbox::new( + shared_string_id, + selection, + ) + .on_click(move |_, _cx| { + if is_default { + prompt_library + .clone() + .remove_prompt_from_default( + prompt_id.clone(), + ) + .log_err(); + } else { + prompt_library + .clone() + .add_prompt_to_default( + prompt_id.clone(), + ) + .log_err(); + } + }), + ) + .child(Label::new( + prompt.title, + )), + ) + .child(div()), + ) + }, + )) + }, + |no_items| { + no_items.child( + Label::new("No prompts").color(Color::Placeholder), + ) + }, + ), + ), + ) + .child( + div() + .id("prompt-preview") + .overflow_y_scroll() + .border_l_1() + .border_color(cx.theme().colors().border) + .size_full() + .flex_none() + .child( + v_flex() + .justify_start() + .py(Spacing::Medium.rems(cx)) + .px(Spacing::Large.rems(cx)) + .gap(Spacing::Large.rems(cx)) + .when_else( + active_prompt.is_some(), + |with_prompt| { + let active_prompt = active_prompt.as_ref().unwrap(); + with_prompt + .child( + v_flex() + .gap_0p5() + .child( + Headline::new( + active_prompt.title.clone(), + ) + .size(HeadlineSize::XSmall), + ) + .child( + h_flex() + .child( + Label::new( + active_prompt + .author + .clone(), + ) + .size(LabelSize::XSmall) + .color(Color::Muted), + ) + .child( + Label::new( + if active_prompt + .languages + .is_empty() + || active_prompt + .languages[0] + == "*" + { + " · Global".to_string() + } else { + format!( + " · {}", + active_prompt + .languages + .join(", ") + ) + }, + ) + .size(LabelSize::XSmall) + .color(Color::Muted), + ), + ), + ) + .child( + div() + .w_full() + .max_w(rems(30.)) + .text_ui(cx) + .child(active_prompt.prompt.clone()), + ) + }, + |without_prompt| { + without_prompt.justify_center().items_center().child( + Label::new("Select a prompt to view details.") + .color(Color::Placeholder), + ) + }, + ), + ), + ), + ) + } +} + +impl EventEmitter for PromptManager {} +impl ModalView for PromptManager {} + +impl FocusableView for PromptManager { + fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle { + self.focus_handle.clone() + } +} diff --git a/crates/ui/src/components/icon.rs b/crates/ui/src/components/icon.rs index 38920d7d22872458931fb02f2c3e12f462523f1d..2f9eae868492332ffde27098ed0e421c64cb7411 100644 --- a/crates/ui/src/components/icon.rs +++ b/crates/ui/src/components/icon.rs @@ -75,20 +75,20 @@ impl IconSize { #[derive(Debug, PartialEq, Copy, Clone, EnumIter)] pub enum IconName { Ai, + ArrowCircle, ArrowDown, ArrowLeft, ArrowRight, ArrowUp, ArrowUpRight, - ArrowCircle, AtSign, AudioOff, AudioOn, Backspace, Bell, + BellDot, BellOff, BellRing, - BellDot, Bolt, CaseSensitive, Check, @@ -96,7 +96,6 @@ pub enum IconName { ChevronLeft, ChevronRight, ChevronUp, - ExpandVertical, Close, Code, Collab, @@ -116,6 +115,7 @@ pub enum IconName { Escape, ExclamationTriangle, Exit, + ExpandVertical, ExternalLink, File, FileDoc, @@ -131,9 +131,11 @@ pub enum IconName { FolderX, Github, Hash, + HistoryRerun, Indicator, IndicatorX, InlayHint, + Library, Link, MagicWand, MagnifyingGlass, @@ -152,58 +154,57 @@ pub enum IconName { Play, Plus, Public, + PullRequest, Quote, Regex, Replace, ReplaceAll, ReplaceNext, - Return, ReplyArrowRight, - Settings, - Sliders, + Return, Screen, SelectAll, Server, + Settings, Shift, + Sliders, Snip, Space, - Split, Spinner, + Split, + Strikethrough, Supermaven, SupermavenDisabled, SupermavenError, SupermavenInit, - Strikethrough, Tab, Terminal, Trash, Update, WholeWord, XCircle, - ZedXCopilot, ZedAssistant, - PullRequest, - HistoryRerun, + ZedXCopilot, } impl IconName { pub fn path(self) -> &'static str { match self { IconName::Ai => "icons/ai.svg", + IconName::ArrowCircle => "icons/arrow_circle.svg", IconName::ArrowDown => "icons/arrow_down.svg", IconName::ArrowLeft => "icons/arrow_left.svg", IconName::ArrowRight => "icons/arrow_right.svg", IconName::ArrowUp => "icons/arrow_up.svg", IconName::ArrowUpRight => "icons/arrow_up_right.svg", - IconName::ArrowCircle => "icons/arrow_circle.svg", IconName::AtSign => "icons/at_sign.svg", IconName::AudioOff => "icons/speaker_off.svg", IconName::AudioOn => "icons/speaker_loud.svg", IconName::Backspace => "icons/backspace.svg", IconName::Bell => "icons/bell.svg", + IconName::BellDot => "icons/bell_dot.svg", IconName::BellOff => "icons/bell_off.svg", IconName::BellRing => "icons/bell_ring.svg", - IconName::BellDot => "icons/bell_dot.svg", IconName::Bolt => "icons/bolt.svg", IconName::CaseSensitive => "icons/case_insensitive.svg", IconName::Check => "icons/check.svg", @@ -211,7 +212,6 @@ impl IconName { IconName::ChevronLeft => "icons/chevron_left.svg", IconName::ChevronRight => "icons/chevron_right.svg", IconName::ChevronUp => "icons/chevron_up.svg", - IconName::ExpandVertical => "icons/expand_vertical.svg", IconName::Close => "icons/x.svg", IconName::Code => "icons/code.svg", IconName::Collab => "icons/user_group_16.svg", @@ -231,6 +231,7 @@ impl IconName { IconName::Escape => "icons/escape.svg", IconName::ExclamationTriangle => "icons/warning.svg", IconName::Exit => "icons/exit.svg", + IconName::ExpandVertical => "icons/expand_vertical.svg", IconName::ExternalLink => "icons/external_link.svg", IconName::File => "icons/file.svg", IconName::FileDoc => "icons/file_icons/book.svg", @@ -246,9 +247,11 @@ impl IconName { IconName::FolderX => "icons/stop_sharing.svg", IconName::Github => "icons/github.svg", IconName::Hash => "icons/hash.svg", + IconName::HistoryRerun => "icons/history_rerun.svg", IconName::Indicator => "icons/indicator.svg", IconName::IndicatorX => "icons/indicator_x.svg", IconName::InlayHint => "icons/inlay_hint.svg", + IconName::Library => "icons/library.svg", IconName::Link => "icons/link.svg", IconName::MagicWand => "icons/magic_wand.svg", IconName::MagnifyingGlass => "icons/magnifying_glass.svg", @@ -262,43 +265,42 @@ impl IconName { IconName::Option => "icons/option.svg", IconName::PageDown => "icons/page_down.svg", IconName::PageUp => "icons/page_up.svg", - IconName::Person => "icons/person.svg", IconName::Pencil => "icons/pencil.svg", + IconName::Person => "icons/person.svg", IconName::Play => "icons/play.svg", IconName::Plus => "icons/plus.svg", IconName::Public => "icons/public.svg", + IconName::PullRequest => "icons/pull_request.svg", IconName::Quote => "icons/quote.svg", IconName::Regex => "icons/regex.svg", IconName::Replace => "icons/replace.svg", IconName::ReplaceAll => "icons/replace_all.svg", IconName::ReplaceNext => "icons/replace_next.svg", - IconName::Return => "icons/return.svg", IconName::ReplyArrowRight => "icons/reply_arrow_right.svg", - IconName::Settings => "icons/file_icons/settings.svg", - IconName::Sliders => "icons/sliders.svg", + IconName::Return => "icons/return.svg", IconName::Screen => "icons/desktop.svg", IconName::SelectAll => "icons/select_all.svg", IconName::Server => "icons/server.svg", + IconName::Settings => "icons/file_icons/settings.svg", IconName::Shift => "icons/shift.svg", + IconName::Sliders => "icons/sliders.svg", IconName::Snip => "icons/snip.svg", IconName::Space => "icons/space.svg", - IconName::Split => "icons/split.svg", IconName::Spinner => "icons/spinner.svg", + IconName::Split => "icons/split.svg", + IconName::Strikethrough => "icons/strikethrough.svg", IconName::Supermaven => "icons/supermaven.svg", IconName::SupermavenDisabled => "icons/supermaven_disabled.svg", IconName::SupermavenError => "icons/supermaven_error.svg", IconName::SupermavenInit => "icons/supermaven_init.svg", - IconName::Strikethrough => "icons/strikethrough.svg", IconName::Tab => "icons/tab.svg", IconName::Terminal => "icons/terminal.svg", IconName::Trash => "icons/trash.svg", IconName::Update => "icons/update.svg", IconName::WholeWord => "icons/word_search.svg", IconName::XCircle => "icons/error.svg", - IconName::ZedXCopilot => "icons/zed_x_copilot.svg", IconName::ZedAssistant => "icons/zed_assistant.svg", - IconName::PullRequest => "icons/pull_request.svg", - IconName::HistoryRerun => "icons/history_rerun.svg", + IconName::ZedXCopilot => "icons/zed_x_copilot.svg", } } } diff --git a/crates/ui/src/components/modal.rs b/crates/ui/src/components/modal.rs index 34e955ec133f053415b5a5a7e63586e613ab4df9..55ba6610550094c593bb5014961f24f823c19571 100644 --- a/crates/ui/src/components/modal.rs +++ b/crates/ui/src/components/modal.rs @@ -3,6 +3,7 @@ use smallvec::SmallVec; use crate::{ h_flex, Clickable, IconButton, IconButtonShape, IconName, Label, LabelCommon, LabelSize, + Spacing, }; #[derive(IntoElement)] @@ -41,11 +42,11 @@ impl ParentElement for ModalHeader { } impl RenderOnce for ModalHeader { - fn render(self, _cx: &mut WindowContext) -> impl IntoElement { + fn render(self, cx: &mut WindowContext) -> impl IntoElement { h_flex() .id(self.id) .w_full() - .px_2() + .px(Spacing::Large.rems(cx)) .py_1p5() .when(self.show_back_button, |this| { this.child( diff --git a/crates/util/src/paths.rs b/crates/util/src/paths.rs index e43c60a5b339925a349565718fc0b633786120dd..02182efc6c96429fe18605b51069205d43d06a88 100644 --- a/crates/util/src/paths.rs +++ b/crates/util/src/paths.rs @@ -24,6 +24,11 @@ lazy_static::lazy_static! { } else { SUPPORT_DIR.join("conversations") }; + pub static ref PROMPTS_DIR: PathBuf = if cfg!(target_os = "macos") { + CONFIG_DIR.join("prompts") + } else { + SUPPORT_DIR.join("prompts") + }; pub static ref EMBEDDINGS_DIR: PathBuf = if cfg!(target_os = "macos") { CONFIG_DIR.join("embeddings") } else { diff --git a/docs/src/assistant-panel.md b/docs/src/assistant-panel.md index 4e6880e9ad7f83e4887a0476fc52922581914fad..7e03aa4ea035423e1d3bb83bee99567d11af373f 100644 --- a/docs/src/assistant-panel.md +++ b/docs/src/assistant-panel.md @@ -133,3 +133,47 @@ You can use Ollama with the Zed assistant by making Ollama appear as an OpenAPI ollama ``` 5. Restart Zed + +## Prompt Manager + +Zed has a prompt manager for enabling and disabling custom prompts. + +These are useful for: + +- Creating a "default prompt" - a super prompt that includes a collection of things you want the assistant to know in every conversation. +- Adding single prompts to your current context to help guide the assistant's responses. +- (In the future) dynamically adding certain prompts to the assistant based on the current context, such as the presence of Rust code or a specific async runtime you want to work with. + +You can access the prompt manager by selecting `Prompt Library...` from the assistant panel's more menu. + +By default when opening the assistant, the prompt manager will load any custom prompts present in your `~/.config/zed/prompts` directory. + +Checked prompts are included in your "default prompt", which can be inserted into the assistant by running `assistant: insert default prompt` or clicking the `Insert Default Prompt` button in the assistant panel's more menu. + +### Creating a custom prompt + +Prompts have a simple format: + +```json +{ + // ~/.config/zed/prompts/no-comments.json + "title": "No comments in code", + "version": "1.0", + "author": "Nate Butler ", + "languages": ["*"], + "prompt": "Do not add inline or doc comments to any returned code. Avoid removing existing comments unless they are no longer accurate due to changes in the code." +} +``` + +Ensure you properly escape your prompt string when creating a new prompt file. + +Example: + +```json +{ + // ... + "prompt": "This project using the gpui crate as it's UI framework for building UI in Rust. When working in Rust files with gpui components, import it's dependencies using `use gpui::{*, prelude::*}`.\n\nWhen a struct has a `#[derive(IntoElement)]` attribute, it is a UI component that must implement `RenderOnce`. Example:\n\n```rust\n#[derive(IntoElement)]\nstruct MyComponent {\n id: ElementId,\n}\n\nimpl MyComponent {\n pub fn new(id: impl Into) -> Self {\n Self { id.into() }\n }\n}\n\nimpl RenderOnce for MyComponent {\n fn render(self, cx: &mut WindowContext) -> impl IntoElement {\n div().id(self.id.clone()).child(text(\"Hello, world!\"))\n }\n}\n```" +} +``` + +In the future we'll allow creating and editing prompts directly in the prompt manager, reducing the need to do this by hand.