diff --git a/assets/icons/library.svg b/assets/icons/library.svg new file mode 100644 index 0000000000..95f8c710c8 --- /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 df75ab6314..cf7242e53a 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 eb463580b8..8d8b5dbdb3 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 0000000000..3490b13478 --- /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 38920d7d22..2f9eae8684 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 34e955ec13..55ba661055 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 e43c60a5b3..02182efc6c 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 4e6880e9ad..7e03aa4ea0 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.