assistant2: Allow creating agent profiles via settings (#27216)

Marshall Bowers created

This PR adds support for creating new agent profiles via the settings:

```json
{
  "assistant": {
    "profiles": {
      "lua": {
        "name": "Lua",
        "tools": {
          "lua-interpreter": true
        }
      },
      "lua-thinking": {
        "name": "Lua + Thinking",
        "tools": {
          "lua-interpreter": true,
          "thinking": true
        }
      }
    }
  }
}
```

Release Notes:

- N/A

Change summary

Cargo.lock                                          |  1 
crates/assistant2/src/assistant.rs                  |  1 
crates/assistant2/src/tool_selector.rs              | 31 +++++++++---
crates/assistant_settings/Cargo.toml                |  1 
crates/assistant_settings/src/agent_profile.rs      |  0 
crates/assistant_settings/src/assistant_settings.rs | 36 +++++++++++++++
6 files changed, 61 insertions(+), 9 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -607,6 +607,7 @@ version = "0.1.0"
 dependencies = [
  "anthropic",
  "anyhow",
+ "collections",
  "deepseek",
  "feature_flags",
  "fs",

crates/assistant2/src/tool_selector.rs 🔗

@@ -1,23 +1,34 @@
 use std::sync::Arc;
 
+use assistant_settings::{AgentProfile, AssistantSettings};
 use assistant_tool::{ToolSource, ToolWorkingSet};
+use collections::HashMap;
 use gpui::Entity;
 use scripting_tool::ScriptingTool;
+use settings::Settings as _;
 use ui::{prelude::*, ContextMenu, PopoverMenu, Tooltip};
 
-use crate::agent_profile::AgentProfile;
-
 pub struct ToolSelector {
-    profiles: Vec<AgentProfile>,
+    profiles: HashMap<Arc<str>, AgentProfile>,
     tools: Arc<ToolWorkingSet>,
 }
 
 impl ToolSelector {
-    pub fn new(tools: Arc<ToolWorkingSet>, _cx: &mut Context<Self>) -> Self {
-        Self {
-            profiles: vec![AgentProfile::read_only(), AgentProfile::code_writer()],
-            tools,
+    pub fn new(tools: Arc<ToolWorkingSet>, cx: &mut Context<Self>) -> Self {
+        let settings = AssistantSettings::get_global(cx);
+        let mut profiles = settings.profiles.clone();
+
+        let read_only = AgentProfile::read_only();
+        if !profiles.contains_key(read_only.name.as_ref()) {
+            profiles.insert(read_only.name.clone().into(), read_only);
+        }
+
+        let code_writer = AgentProfile::code_writer();
+        if !profiles.contains_key(code_writer.name.as_ref()) {
+            profiles.insert(code_writer.name.clone().into(), code_writer);
         }
+
+        Self { profiles, tools }
     }
 
     fn build_context_menu(
@@ -31,7 +42,7 @@ impl ToolSelector {
             let icon_position = IconPosition::End;
 
             menu = menu.header("Profiles");
-            for profile in profiles.clone() {
+            for (_id, profile) in profiles.clone() {
                 menu = menu.toggleable_entry(profile.name.clone(), false, icon_position, None, {
                     let tools = tool_set.clone();
                     move |_window, cx| {
@@ -44,6 +55,10 @@ impl ToolSelector {
                                 .filter_map(|(tool, enabled)| enabled.then(|| tool.clone()))
                                 .collect::<Vec<_>>(),
                         );
+
+                        if profile.tools.contains_key(ScriptingTool::NAME) {
+                            tools.enable_scripting_tool();
+                        }
                     }
                 });
             }

crates/assistant_settings/Cargo.toml 🔗

@@ -14,6 +14,7 @@ path = "src/assistant_settings.rs"
 [dependencies]
 anthropic = { workspace = true, features = ["schemars"] }
 anyhow.workspace = true
+collections.workspace = true
 feature_flags.workspace = true
 gpui.workspace = true
 language_model.workspace = true

crates/assistant_settings/src/assistant_settings.rs 🔗

@@ -1,7 +1,10 @@
+mod agent_profile;
+
 use std::sync::Arc;
 
 use ::open_ai::Model as OpenAiModel;
 use anthropic::Model as AnthropicModel;
+use collections::HashMap;
 use deepseek::Model as DeepseekModel;
 use feature_flags::FeatureFlagAppExt;
 use gpui::{App, Pixels};
@@ -12,6 +15,8 @@ use schemars::{schema::Schema, JsonSchema};
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources};
 
+pub use crate::agent_profile::*;
+
 #[derive(Copy, Clone, Default, Debug, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 pub enum AssistantDockPosition {
@@ -66,6 +71,7 @@ pub struct AssistantSettings {
     pub inline_alternatives: Vec<LanguageModelSelection>,
     pub using_outdated_settings_version: bool,
     pub enable_experimental_live_diffs: bool,
+    pub profiles: HashMap<Arc<str>, AgentProfile>,
 }
 
 impl AssistantSettings {
@@ -166,6 +172,7 @@ impl AssistantSettingsContent {
                     editor_model: None,
                     inline_alternatives: None,
                     enable_experimental_live_diffs: None,
+                    profiles: None,
                 },
                 VersionedAssistantSettingsContent::V2(settings) => settings.clone(),
             },
@@ -187,6 +194,7 @@ impl AssistantSettingsContent {
                 editor_model: None,
                 inline_alternatives: None,
                 enable_experimental_live_diffs: None,
+                profiles: None,
             },
         }
     }
@@ -316,6 +324,7 @@ impl Default for VersionedAssistantSettingsContent {
             editor_model: None,
             inline_alternatives: None,
             enable_experimental_live_diffs: None,
+            profiles: None,
         })
     }
 }
@@ -352,6 +361,8 @@ pub struct AssistantSettingsContentV2 {
     ///
     /// Default: false
     enable_experimental_live_diffs: Option<bool>,
+    #[schemars(skip)]
+    profiles: Option<HashMap<Arc<str>, AgentProfileContent>>,
 }
 
 #[derive(Clone, Debug, Serialize, Deserialize, JsonSchema, PartialEq)]
@@ -388,6 +399,12 @@ impl Default for LanguageModelSelection {
     }
 }
 
+#[derive(Debug, PartialEq, Clone, Serialize, Deserialize, JsonSchema)]
+pub struct AgentProfileContent {
+    pub name: Arc<str>,
+    pub tools: HashMap<Arc<str>, bool>,
+}
+
 #[derive(Clone, Serialize, Deserialize, JsonSchema, Debug)]
 pub struct AssistantSettingsContentV1 {
     /// Whether the Assistant is enabled.
@@ -482,6 +499,24 @@ impl Settings for AssistantSettings {
                 &mut settings.enable_experimental_live_diffs,
                 value.enable_experimental_live_diffs,
             );
+            merge(
+                &mut settings.profiles,
+                value.profiles.map(|profiles| {
+                    profiles
+                        .into_iter()
+                        .map(|(id, profile)| {
+                            (
+                                id,
+                                AgentProfile {
+                                    name: profile.name.into(),
+                                    tools: profile.tools,
+                                    context_servers: HashMap::default(),
+                                },
+                            )
+                        })
+                        .collect()
+                }),
+            );
         }
 
         Ok(settings)
@@ -546,6 +581,7 @@ mod tests {
                             default_width: None,
                             default_height: None,
                             enable_experimental_live_diffs: None,
+                            profiles: None,
                         }),
                     )
                 },