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.