git: Allow customising commit message prompt from rules library (#45004)

Bennet Bo Fenner and Danilo Leal created

Closes #26823 

Release Notes:

- Added support for customising the prompt used for generating commit
message in the rules library

---------

Co-authored-by: Danilo Leal <daniloleal09@gmail.com>

Change summary

crates/agent/src/agent.rs                  |   5 
crates/agent_ui/src/completion_provider.rs |  13 
crates/git_ui/src/git_panel.rs             |  22 +
crates/prompt_store/src/prompt_store.rs    |  53 ++
crates/rules_library/src/rules_library.rs  | 394 ++++++++++++-----------
5 files changed, 285 insertions(+), 202 deletions(-)

Detailed changes

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,
                     }),

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::<Vec<_>>()

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<Self>) {
         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 {

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<UserPromptId> {
+        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<Self>,
     ) -> Task<Result<()>> {
-        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<Result<()>> {
         let mut cache = self.metadata_cache.write();
 
-        if id.is_built_in() {
+        if !id.can_edit() {
             title = cache
                 .metadata_by_id
                 .get(&id)

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<Picker<Self>>,
     ) -> Option<Self::ListItem> {
         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::<IconButton>(default.then(|| {
+                        .end_slot::<IconButton>((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<Self>) {
         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<Self>,
+    ) {
+        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<Self>,
+    ) {
+        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<Editor>,
+        read_only: bool,
         cx: &mut Context<Self>,
     ) -> 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<RulesLibrary>) -> gpui::Stateful<Div> {
         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)