assistant: Put `/docs` and `/project` behind a setting (#16186)

Marshall Bowers created

This PR puts the availability of the `/docs` and `/project` slash
commands behind their respective settings.

Release Notes:

- N/A

Change summary

assets/settings/default.json                                 | 13 +
crates/assistant/src/assistant.rs                            | 35 +++-
crates/assistant/src/slash_command/docs_command.rs           |  7 
crates/assistant/src/slash_command_settings.rs               | 44 ++++++
crates/assistant_slash_command/src/slash_command_registry.rs |  8 +
5 files changed, 91 insertions(+), 16 deletions(-)

Detailed changes

assets/settings/default.json 🔗

@@ -400,6 +400,19 @@
       "model": "gpt-4o"
     }
   },
+  // The settings for slash commands.
+  "slash_commands": {
+    // Settings for the `/docs` slash command.
+    "docs": {
+      // Whether `/docs` is enabled.
+      "enabled": false
+    },
+    // Settings for the `/project` slash command.
+    "project": {
+      // Whether `/project` is enabled.
+      "enabled": false
+    }
+  },
   // Whether the screen sharing icon is shown in the os status bar.
   "show_call_status_icon": true,
   // Whether to use language servers to provide code intelligence.

crates/assistant/src/assistant.rs 🔗

@@ -10,6 +10,7 @@ mod model_selector;
 mod prompt_library;
 mod prompts;
 mod slash_command;
+pub mod slash_command_settings;
 mod streaming_diff;
 mod terminal_inline_assistant;
 
@@ -43,6 +44,8 @@ use std::sync::Arc;
 pub(crate) use streaming_diff::*;
 use util::ResultExt;
 
+use crate::slash_command_settings::SlashCommandSettings;
+
 actions!(
     assistant,
     [
@@ -177,6 +180,7 @@ pub fn init(
 ) -> Arc<PromptBuilder> {
     cx.set_global(Assistant::default());
     AssistantSettings::register(cx);
+    SlashCommandSettings::register(cx);
 
     // TODO: remove this when 0.148.0 is released.
     if AssistantSettings::get_global(cx).using_outdated_settings_version {
@@ -290,6 +294,7 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
     slash_command_registry.register_command(terminal_command::TerminalSlashCommand, true);
     slash_command_registry.register_command(now_command::NowSlashCommand, false);
     slash_command_registry.register_command(diagnostics_command::DiagnosticsSlashCommand, true);
+
     if let Some(prompt_builder) = prompt_builder {
         slash_command_registry.register_command(
             workflow_command::WorkflowSlashCommand::new(prompt_builder),
@@ -298,15 +303,10 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
     }
     slash_command_registry.register_command(fetch_command::FetchSlashCommand, false);
 
-    cx.observe_flag::<docs_command::DocsSlashCommandFeatureFlag, _>({
-        let slash_command_registry = slash_command_registry.clone();
-        move |is_enabled, _cx| {
-            if is_enabled {
-                slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
-            }
-        }
-    })
-    .detach();
+    update_slash_commands_from_settings(cx);
+    cx.observe_global::<SettingsStore>(update_slash_commands_from_settings)
+        .detach();
+
     cx.observe_flag::<search_command::SearchSlashCommandFeatureFlag, _>({
         let slash_command_registry = slash_command_registry.clone();
         move |is_enabled, _cx| {
@@ -318,6 +318,23 @@ fn register_slash_commands(prompt_builder: Option<Arc<PromptBuilder>>, cx: &mut
     .detach();
 }
 
+fn update_slash_commands_from_settings(cx: &mut AppContext) {
+    let slash_command_registry = SlashCommandRegistry::global(cx);
+    let settings = SlashCommandSettings::get_global(cx);
+
+    if settings.docs.enabled {
+        slash_command_registry.register_command(docs_command::DocsSlashCommand, true);
+    } else {
+        slash_command_registry.unregister_command(docs_command::DocsSlashCommand);
+    }
+
+    if settings.project.enabled {
+        slash_command_registry.register_command(project_command::ProjectSlashCommand, true);
+    } else {
+        slash_command_registry.unregister_command(project_command::ProjectSlashCommand);
+    }
+}
+
 pub fn humanize_token_count(count: usize) -> String {
     match count {
         0..=999 => count.to_string(),

crates/assistant/src/slash_command/docs_command.rs 🔗

@@ -7,7 +7,6 @@ use anyhow::{anyhow, bail, Result};
 use assistant_slash_command::{
     ArgumentCompletion, SlashCommand, SlashCommandOutput, SlashCommandOutputSection,
 };
-use feature_flags::FeatureFlag;
 use gpui::{AppContext, BackgroundExecutor, Model, Task, WeakView};
 use indexed_docs::{
     DocsDotRsProvider, IndexedDocsRegistry, IndexedDocsStore, LocalRustdocProvider, PackageName,
@@ -19,12 +18,6 @@ use ui::prelude::*;
 use util::{maybe, ResultExt};
 use workspace::Workspace;
 
-pub(crate) struct DocsSlashCommandFeatureFlag;
-
-impl FeatureFlag for DocsSlashCommandFeatureFlag {
-    const NAME: &'static str = "docs-slash-command";
-}
-
 pub(crate) struct DocsSlashCommand;
 
 impl DocsSlashCommand {

crates/assistant/src/slash_command_settings.rs 🔗

@@ -0,0 +1,44 @@
+use anyhow::Result;
+use gpui::AppContext;
+use schemars::JsonSchema;
+use serde::{Deserialize, Serialize};
+use settings::{Settings, SettingsSources};
+
+/// Settings for slash commands.
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+pub struct SlashCommandSettings {
+    /// Settings for the `/docs` slash command.
+    #[serde(default)]
+    pub docs: DocsCommandSettings,
+    /// Settings for the `/project` slash command.
+    #[serde(default)]
+    pub project: ProjectCommandSettings,
+}
+
+/// Settings for the `/docs` slash command.
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+pub struct DocsCommandSettings {
+    /// Whether `/docs` is enabled.
+    #[serde(default)]
+    pub enabled: bool,
+}
+
+/// Settings for the `/project` slash command.
+#[derive(Deserialize, Serialize, Debug, Default, Clone, JsonSchema)]
+pub struct ProjectCommandSettings {
+    /// Whether `/project` is enabled.
+    #[serde(default)]
+    pub enabled: bool,
+}
+
+impl Settings for SlashCommandSettings {
+    const KEY: Option<&'static str> = Some("slash_commands");
+
+    type FileContent = Self;
+
+    fn load(sources: SettingsSources<Self::FileContent>, _cx: &mut AppContext) -> Result<Self> {
+        SettingsSources::<Self::FileContent>::json_merge_with(
+            [sources.default].into_iter().chain(sources.user),
+        )
+    }
+}

crates/assistant_slash_command/src/slash_command_registry.rs 🔗

@@ -56,6 +56,14 @@ impl SlashCommandRegistry {
         state.commands.insert(command_name, Arc::new(command));
     }
 
+    /// Unregisters the provided [`SlashCommand`].
+    pub fn unregister_command(&self, command: impl SlashCommand) {
+        let mut state = self.state.write();
+        let command_name: Arc<str> = command.name().into();
+        state.featured_commands.remove(&command_name);
+        state.commands.remove(&command_name);
+    }
+
     /// Returns the names of registered [`SlashCommand`]s.
     pub fn command_names(&self) -> Vec<Arc<str>> {
         self.state.read().commands.keys().cloned().collect()