diff --git a/crates/agent/src/agent.rs b/crates/agent/src/agent.rs index 715c8682ba9b1e6d21c6558271c00180691f59f0..f29b9f405c121b54fd9a4a250e977c593ffc3d4b 100644 --- a/crates/agent/src/agent.rs +++ b/crates/agent/src/agent.rs @@ -414,10 +414,7 @@ impl NativeAgent { .into_iter() .flat_map(|(contents, prompt_metadata)| match contents { Ok(contents) => Some(UserRulesContext { - uuid: match prompt_metadata.id { - prompt_store::PromptId::User { uuid } => uuid, - prompt_store::PromptId::EditWorkflow => return None, - }, + uuid: prompt_metadata.id.user_id()?, title: prompt_metadata.title.map(|title| title.to_string()), contents, }), diff --git a/crates/agent_ui/src/completion_provider.rs b/crates/agent_ui/src/completion_provider.rs index a2b6e0510e25c12cfbfb98d3e72cb0d2c830887a..206a2b3282b5471e8d5e8d18788519c3853dca55 100644 --- a/crates/agent_ui/src/completion_provider.rs +++ b/crates/agent_ui/src/completion_provider.rs @@ -20,7 +20,7 @@ use project::{ Completion, CompletionDisplayOptions, CompletionIntent, CompletionResponse, PathMatchCandidateSet, Project, ProjectPath, Symbol, WorktreeId, }; -use prompt_store::{PromptId, PromptStore, UserPromptId}; +use prompt_store::{PromptStore, UserPromptId}; use rope::Point; use text::{Anchor, ToPoint as _}; use ui::prelude::*; @@ -1585,13 +1585,10 @@ pub(crate) fn search_rules( if metadata.default { None } else { - match metadata.id { - PromptId::EditWorkflow => None, - PromptId::User { uuid } => Some(RulesContextEntry { - prompt_id: uuid, - title: metadata.title?, - }), - } + Some(RulesContextEntry { + prompt_id: metadata.id.user_id()?, + title: metadata.title?, + }) } }) .collect::>() diff --git a/crates/git_ui/src/git_panel.rs b/crates/git_ui/src/git_panel.rs index 9bdcb0858a4547b3312ed5d48c85a6e8a7015d8d..d7be4dfe723e9cbe312420e920e4b4e90f080585 100644 --- a/crates/git_ui/src/git_panel.rs +++ b/crates/git_ui/src/git_panel.rs @@ -57,7 +57,7 @@ use project::{ git_store::{GitStoreEvent, Repository, RepositoryEvent, RepositoryId, pending_op}, project_settings::{GitPathStyle, ProjectSettings}, }; -use prompt_store::RULES_FILE_NAMES; +use prompt_store::{PromptId, PromptStore, RULES_FILE_NAMES}; use serde::{Deserialize, Serialize}; use settings::{Settings, SettingsStore, StatusStyle}; use std::future::Future; @@ -2422,6 +2422,20 @@ impl GitPanel { } } + async fn load_commit_message_prompt(cx: &mut AsyncApp) -> String { + const DEFAULT_PROMPT: &str = include_str!("commit_message_prompt.txt"); + + let load = async { + let store = cx.update(|cx| PromptStore::global(cx)).ok()?.await.ok()?; + store + .update(cx, |s, cx| s.load(PromptId::CommitMessage, cx)) + .ok()? + .await + .ok() + }; + load.await.unwrap_or_else(|| DEFAULT_PROMPT.to_string()) + } + /// Generates a commit message using an LLM. pub fn generate_commit_message(&mut self, cx: &mut Context) { if !self.can_commit() || !AgentSettings::get_global(cx).enabled(cx) { @@ -2487,14 +2501,14 @@ impl GitPanel { let rules_content = Self::load_project_rules(&project, &repo_work_dir, &mut cx).await; + let prompt = Self::load_commit_message_prompt(&mut cx).await; + let subject = this.update(cx, |this, cx| { this.commit_editor.read(cx).text(cx).lines().next().map(ToOwned::to_owned).unwrap_or_default() })?; let text_empty = subject.trim().is_empty(); - const PROMPT: &str = include_str!("commit_message_prompt.txt"); - let rules_section = match &rules_content { Some(rules) => format!( "\n\nThe user has provided the following project rules that you should follow when writing the commit message:\n\ @@ -2510,7 +2524,7 @@ impl GitPanel { }; let content = format!( - "{PROMPT}{rules_section}{subject_section}\nHere are the changes in this commit:\n{diff_text}" + "{prompt}{rules_section}{subject_section}\nHere are the changes in this commit:\n{diff_text}" ); let request = LanguageModelRequest { diff --git a/crates/prompt_store/src/prompt_store.rs b/crates/prompt_store/src/prompt_store.rs index fb087ce34d6d67fe4ea11a33f554307ed558c18a..b69051b067c95674d8d09be76c5b4c607fd03f67 100644 --- a/crates/prompt_store/src/prompt_store.rs +++ b/crates/prompt_store/src/prompt_store.rs @@ -56,6 +56,7 @@ pub struct PromptMetadata { pub enum PromptId { User { uuid: UserPromptId }, EditWorkflow, + CommitMessage, } impl PromptId { @@ -63,8 +64,32 @@ impl PromptId { UserPromptId::new().into() } + pub fn user_id(&self) -> Option { + match self { + Self::User { uuid } => Some(*uuid), + _ => None, + } + } + pub fn is_built_in(&self) -> bool { - !matches!(self, PromptId::User { .. }) + match self { + Self::User { .. } => false, + Self::EditWorkflow | Self::CommitMessage => true, + } + } + + pub fn can_edit(&self) -> bool { + match self { + Self::User { .. } | Self::CommitMessage => true, + Self::EditWorkflow => false, + } + } + + pub fn default_content(&self) -> Option<&'static str> { + match self { + Self::User { .. } | Self::EditWorkflow => None, + Self::CommitMessage => Some(include_str!("../../git_ui/src/commit_message_prompt.txt")), + } } } @@ -95,6 +120,7 @@ impl std::fmt::Display for PromptId { match self { PromptId::User { uuid } => write!(f, "{}", uuid.0), PromptId::EditWorkflow => write!(f, "Edit workflow"), + PromptId::CommitMessage => write!(f, "Commit message"), } } } @@ -181,6 +207,25 @@ impl PromptStore { metadata.delete(&mut txn, &PromptId::EditWorkflow).ok(); bodies.delete(&mut txn, &PromptId::EditWorkflow).ok(); + // Insert default commit message prompt if not present + if metadata.get(&txn, &PromptId::CommitMessage)?.is_none() { + metadata.put( + &mut txn, + &PromptId::CommitMessage, + &PromptMetadata { + id: PromptId::CommitMessage, + title: Some("Git Commit Message".into()), + default: false, + saved_at: Utc::now(), + }, + )?; + } + if bodies.get(&txn, &PromptId::CommitMessage)?.is_none() { + let commit_message_prompt = + include_str!("../../git_ui/src/commit_message_prompt.txt"); + bodies.put(&mut txn, &PromptId::CommitMessage, commit_message_prompt)?; + } + txn.commit()?; Self::upgrade_dbs(&db_env, metadata, bodies).log_err(); @@ -387,8 +432,8 @@ impl PromptStore { body: Rope, cx: &Context, ) -> Task> { - if id.is_built_in() { - return Task::ready(Err(anyhow!("built-in prompts cannot be saved"))); + if !id.can_edit() { + return Task::ready(Err(anyhow!("this prompt cannot be edited"))); } let prompt_metadata = PromptMetadata { @@ -430,7 +475,7 @@ impl PromptStore { ) -> Task> { let mut cache = self.metadata_cache.write(); - if id.is_built_in() { + if !id.can_edit() { title = cache .metadata_by_id .get(&id) diff --git a/crates/rules_library/src/rules_library.rs b/crates/rules_library/src/rules_library.rs index 09b7e0b539cde7371b97ef092fbd8f904b241c13..00cf939f7af45f7701cd9d3599a103ece4a6f393 100644 --- a/crates/rules_library/src/rules_library.rs +++ b/crates/rules_library/src/rules_library.rs @@ -21,9 +21,7 @@ use std::sync::atomic::AtomicBool; use std::time::Duration; use theme::ThemeSettings; use title_bar::platform_title_bar::PlatformTitleBar; -use ui::{ - Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Render, Tooltip, prelude::*, -}; +use ui::{Divider, KeyBinding, ListItem, ListItemSpacing, ListSubHeader, Tooltip, prelude::*}; use util::{ResultExt, TryFutureExt}; use workspace::{Workspace, WorkspaceSettings, client_side_decorations}; use zed_actions::assistant::InlineAssist; @@ -44,15 +42,12 @@ actions!( /// Duplicates the selected rule. DuplicateRule, /// Toggles whether the selected rule is a default rule. - ToggleDefaultRule + ToggleDefaultRule, + /// Restores a built-in rule to its default content. + RestoreDefaultContent ] ); -const BUILT_IN_TOOLTIP_TEXT: &str = concat!( - "This rule supports special functionality.\n", - "It's read-only, but you can remove it from your default rules." -); - pub trait InlineAssistDelegate { fn assist( &self, @@ -270,23 +265,35 @@ impl PickerDelegate for RulePickerDelegate { .background_spawn(async move { let matches = search.await; - let (default_rules, non_default_rules): (Vec<_>, Vec<_>) = - matches.iter().partition(|rule| rule.default); + let (built_in_rules, user_rules): (Vec<_>, Vec<_>) = + matches.into_iter().partition(|rule| rule.id.is_built_in()); + let (default_rules, other_rules): (Vec<_>, Vec<_>) = + user_rules.into_iter().partition(|rule| rule.default); let mut filtered_entries = Vec::new(); + if !built_in_rules.is_empty() { + filtered_entries.push(RulePickerEntry::Header("Built-in Rules".into())); + + for rule in built_in_rules { + filtered_entries.push(RulePickerEntry::Rule(rule)); + } + + filtered_entries.push(RulePickerEntry::Separator); + } + if !default_rules.is_empty() { filtered_entries.push(RulePickerEntry::Header("Default Rules".into())); for rule in default_rules { - filtered_entries.push(RulePickerEntry::Rule(rule.clone())); + filtered_entries.push(RulePickerEntry::Rule(rule)); } filtered_entries.push(RulePickerEntry::Separator); } - for rule in non_default_rules { - filtered_entries.push(RulePickerEntry::Rule(rule.clone())); + for rule in other_rules { + filtered_entries.push(RulePickerEntry::Rule(rule)); } let selected_index = prev_prompt_id @@ -341,21 +348,27 @@ impl PickerDelegate for RulePickerDelegate { cx: &mut Context>, ) -> Option { match self.filtered_entries.get(ix)? { - RulePickerEntry::Header(title) => Some( - ListSubHeader::new(title.clone()) - .end_slot( - IconButton::new("info", IconName::Info) - .style(ButtonStyle::Transparent) - .icon_size(IconSize::Small) - .icon_color(Color::Muted) - .tooltip(Tooltip::text( - "Default Rules are attached by default with every new thread.", - )) - .into_any_element(), - ) - .inset(true) - .into_any_element(), - ), + RulePickerEntry::Header(title) => { + let tooltip_text = if title.as_ref() == "Built-in Rules" { + "Built-in rules are those included out of the box with Zed." + } else { + "Default Rules are attached by default with every new thread." + }; + + Some( + ListSubHeader::new(title.clone()) + .end_slot( + IconButton::new("info", IconName::Info) + .style(ButtonStyle::Transparent) + .icon_size(IconSize::Small) + .icon_color(Color::Muted) + .tooltip(Tooltip::text(tooltip_text)) + .into_any_element(), + ) + .inset(true) + .into_any_element(), + ) + } RulePickerEntry::Separator => Some( h_flex() .py_1() @@ -376,7 +389,7 @@ impl PickerDelegate for RulePickerDelegate { .truncate() .mr_10(), ) - .end_slot::(default.then(|| { + .end_slot::((default && !prompt_id.is_built_in()).then(|| { IconButton::new("toggle-default-rule", IconName::Paperclip) .toggle_state(true) .icon_color(Color::Accent) @@ -386,62 +399,52 @@ impl PickerDelegate for RulePickerDelegate { cx.emit(RulePickerEvent::ToggledDefault { prompt_id }) })) })) - .end_hover_slot( - h_flex() - .child(if prompt_id.is_built_in() { - div() - .id("built-in-rule") - .child(Icon::new(IconName::FileLock).color(Color::Muted)) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Built-in rule", - None, - BUILT_IN_TOOLTIP_TEXT, - cx, - ) - }) - .into_any() - } else { - IconButton::new("delete-rule", IconName::Trash) - .icon_color(Color::Muted) - .icon_size(IconSize::Small) - .tooltip(Tooltip::text("Delete Rule")) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::Deleted { prompt_id }) - })) - .into_any_element() - }) - .child( - IconButton::new("toggle-default-rule", IconName::Plus) - .selected_icon(IconName::Dash) - .toggle_state(default) - .icon_size(IconSize::Small) - .icon_color(if default { - Color::Accent - } else { - Color::Muted - }) - .map(|this| { - if default { - this.tooltip(Tooltip::text( - "Remove from Default Rules", - )) + .when(!prompt_id.is_built_in(), |this| { + this.end_hover_slot( + h_flex() + .child( + IconButton::new("delete-rule", IconName::Trash) + .icon_color(Color::Muted) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text("Delete Rule")) + .on_click(cx.listener(move |_, _, _, cx| { + cx.emit(RulePickerEvent::Deleted { prompt_id }) + })), + ) + .child( + IconButton::new("toggle-default-rule", IconName::Plus) + .selected_icon(IconName::Dash) + .toggle_state(default) + .icon_size(IconSize::Small) + .icon_color(if default { + Color::Accent } else { - this.tooltip(move |_window, cx| { - Tooltip::with_meta( - "Add to Default Rules", - None, - "Always included in every thread.", - cx, - ) + Color::Muted + }) + .map(|this| { + if default { + this.tooltip(Tooltip::text( + "Remove from Default Rules", + )) + } else { + this.tooltip(move |_window, cx| { + Tooltip::with_meta( + "Add to Default Rules", + None, + "Always included in every thread.", + cx, + ) + }) + } + }) + .on_click(cx.listener(move |_, _, _, cx| { + cx.emit(RulePickerEvent::ToggledDefault { + prompt_id, }) - } - }) - .on_click(cx.listener(move |_, _, _, cx| { - cx.emit(RulePickerEvent::ToggledDefault { prompt_id }) - })), - ), - ) + })), + ), + ) + }) .into_any_element(), ) } @@ -573,7 +576,7 @@ impl RulesLibrary { pub fn save_rule(&mut self, prompt_id: PromptId, window: &mut Window, cx: &mut Context) { const SAVE_THROTTLE: Duration = Duration::from_millis(500); - if prompt_id.is_built_in() { + if !prompt_id.can_edit() { return; } @@ -661,6 +664,33 @@ impl RulesLibrary { } } + pub fn restore_default_content_for_active_rule( + &mut self, + window: &mut Window, + cx: &mut Context, + ) { + if let Some(active_rule_id) = self.active_rule_id { + self.restore_default_content(active_rule_id, window, cx); + } + } + + pub fn restore_default_content( + &mut self, + prompt_id: PromptId, + window: &mut Window, + cx: &mut Context, + ) { + let Some(default_content) = prompt_id.default_content() else { + return; + }; + + if let Some(rule_editor) = self.rule_editors.get(&prompt_id) { + rule_editor.body_editor.update(cx, |editor, cx| { + editor.set_text(default_content, window, cx); + }); + } + } + pub fn toggle_default_for_rule( &mut self, prompt_id: PromptId, @@ -721,7 +751,7 @@ impl RulesLibrary { }); let mut editor = Editor::for_buffer(buffer, None, window, cx); - if prompt_id.is_built_in() { + if !prompt_id.can_edit() { editor.set_read_only(true); editor.set_show_edit_predictions(Some(false), window, cx); } @@ -1148,30 +1178,38 @@ impl RulesLibrary { fn render_active_rule_editor( &self, editor: &Entity, + read_only: bool, cx: &mut Context, ) -> impl IntoElement { let settings = ThemeSettings::get_global(cx); + let text_color = if read_only { + cx.theme().colors().text_muted + } else { + cx.theme().colors().text + }; div() .w_full() - .on_action(cx.listener(Self::move_down_from_title)) .pl_1() .border_1() .border_color(transparent_black()) .rounded_sm() - .group_hover("active-editor-header", |this| { - this.border_color(cx.theme().colors().border_variant) + .when(!read_only, |this| { + this.group_hover("active-editor-header", |this| { + this.border_color(cx.theme().colors().border_variant) + }) }) + .on_action(cx.listener(Self::move_down_from_title)) .child(EditorElement::new( &editor, EditorStyle { background: cx.theme().system().transparent, local_player: cx.theme().players().local(), text: TextStyle { - color: cx.theme().colors().editor_foreground, + color: text_color, font_family: settings.ui_font.family.clone(), font_features: settings.ui_font.features.clone(), - font_size: HeadlineSize::Large.rems().into(), + font_size: HeadlineSize::Medium.rems().into(), font_weight: settings.ui_font.weight, line_height: relative(settings.buffer_line_height.value()), ..Default::default() @@ -1186,6 +1224,68 @@ impl RulesLibrary { )) } + fn render_duplicate_rule_button(&self) -> impl IntoElement { + IconButton::new("duplicate-rule", IconName::BookCopy) + .tooltip(move |_window, cx| Tooltip::for_action("Duplicate Rule", &DuplicateRule, cx)) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(DuplicateRule), cx); + }) + } + + fn render_built_in_rule_controls(&self) -> impl IntoElement { + h_flex() + .gap_1() + .child(self.render_duplicate_rule_button()) + .child( + IconButton::new("restore-default", IconName::RotateCcw) + .tooltip(move |_window, cx| { + Tooltip::for_action( + "Restore to Default Content", + &RestoreDefaultContent, + cx, + ) + }) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(RestoreDefaultContent), cx); + }), + ) + } + + fn render_regular_rule_controls(&self, default: bool) -> impl IntoElement { + h_flex() + .gap_1() + .child( + IconButton::new("toggle-default-rule", IconName::Paperclip) + .toggle_state(default) + .when(default, |this| this.icon_color(Color::Accent)) + .map(|this| { + if default { + this.tooltip(Tooltip::text("Remove from Default Rules")) + } else { + this.tooltip(move |_window, cx| { + Tooltip::with_meta( + "Add to Default Rules", + None, + "Always included in every thread.", + cx, + ) + }) + } + }) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(ToggleDefaultRule), cx); + }), + ) + .child(self.render_duplicate_rule_button()) + .child( + IconButton::new("delete-rule", IconName::Trash) + .tooltip(move |_window, cx| Tooltip::for_action("Delete Rule", &DeleteRule, cx)) + .on_click(|_, window, cx| { + window.dispatch_action(Box::new(DeleteRule), cx); + }), + ) + } + fn render_active_rule(&mut self, cx: &mut Context) -> gpui::Stateful
{ div() .id("rule-editor") @@ -1198,9 +1298,9 @@ impl RulesLibrary { let rule_metadata = self.store.read(cx).metadata(prompt_id)?; let rule_editor = &self.rule_editors[&prompt_id]; let focus_handle = rule_editor.body_editor.focus_handle(cx); - let model = LanguageModelRegistry::read_global(cx) - .default_model() - .map(|default| default.model); + let registry = LanguageModelRegistry::read_global(cx); + let model = registry.default_model().map(|default| default.model); + let built_in = prompt_id.is_built_in(); Some( v_flex() @@ -1214,14 +1314,15 @@ impl RulesLibrary { .child( h_flex() .group("active-editor-header") - .pt_2() - .pl_1p5() - .pr_2p5() + .h_12() + .px_2() .gap_2() .justify_between() - .child( - self.render_active_rule_editor(&rule_editor.title_editor, cx), - ) + .child(self.render_active_rule_editor( + &rule_editor.title_editor, + built_in, + cx, + )) .child( h_flex() .h_full() @@ -1258,89 +1359,15 @@ impl RulesLibrary { .color(Color::Muted), ) })) - .child(if prompt_id.is_built_in() { - div() - .id("built-in-rule") - .child( - Icon::new(IconName::FileLock) - .color(Color::Muted), - ) - .tooltip(move |_window, cx| { - Tooltip::with_meta( - "Built-in rule", - None, - BUILT_IN_TOOLTIP_TEXT, - cx, - ) - }) - .into_any() - } else { - IconButton::new("delete-rule", IconName::Trash) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Delete Rule", - &DeleteRule, - cx, - ) - }) - .on_click(|_, window, cx| { - window - .dispatch_action(Box::new(DeleteRule), cx); - }) - .into_any_element() - }) - .child( - IconButton::new("duplicate-rule", IconName::BookCopy) - .tooltip(move |_window, cx| { - Tooltip::for_action( - "Duplicate Rule", - &DuplicateRule, - cx, - ) - }) - .on_click(|_, window, cx| { - window.dispatch_action( - Box::new(DuplicateRule), - cx, - ); - }), - ) - .child( - IconButton::new( - "toggle-default-rule", - IconName::Paperclip, - ) - .toggle_state(rule_metadata.default) - .icon_color(if rule_metadata.default { - Color::Accent + .map(|this| { + if built_in { + this.child(self.render_built_in_rule_controls()) } else { - Color::Muted - }) - .map(|this| { - if rule_metadata.default { - this.tooltip(Tooltip::text( - "Remove from Default Rules", - )) - } else { - this.tooltip(move |_window, cx| { - Tooltip::with_meta( - "Add to Default Rules", - None, - "Always included in every thread.", - cx, - ) - }) - } - }) - .on_click( - |_, window, cx| { - window.dispatch_action( - Box::new(ToggleDefaultRule), - cx, - ); - }, - ), - ), + this.child(self.render_regular_rule_controls( + rule_metadata.default, + )) + } + }), ), ) .child( @@ -1385,6 +1412,9 @@ impl Render for RulesLibrary { .on_action(cx.listener(|this, &ToggleDefaultRule, window, cx| { this.toggle_default_for_active_rule(window, cx) })) + .on_action(cx.listener(|this, &RestoreDefaultContent, window, cx| { + this.restore_default_content_for_active_rule(window, cx) + })) .size_full() .overflow_hidden() .font(ui_font)