acp: Support launching custom agent servers (#36805)

Antonio Scandurra created

It's enough to add this to your settings:

```json
{
    "agent_servers": {
        "Name Of Your Agent": {
            "command": "/path/to/custom/agent",
            "args": ["arguments", "that", "you", "want"],
        }
    }
}
```

Release Notes:

- N/A

Change summary

crates/acp_tools/src/acp_tools.rs                |  12 +-
crates/agent2/src/native_agent_server.rs         |  12 +-
crates/agent_servers/src/acp.rs                  |  12 +-
crates/agent_servers/src/agent_servers.rs        |   8 
crates/agent_servers/src/claude.rs               |  12 +-
crates/agent_servers/src/custom.rs               |  59 ++++++++++
crates/agent_servers/src/e2e_tests.rs            |  13 +-
crates/agent_servers/src/gemini.rs               |  13 +-
crates/agent_servers/src/settings.rs             |  24 +++
crates/agent_ui/src/acp/thread_view.rs           |  20 +-
crates/agent_ui/src/agent_panel.rs               | 105 ++++++++++++++---
crates/agent_ui/src/agent_ui.rs                  |  19 ++
crates/language_model/src/language_model.rs      |   4 
crates/language_models/src/provider/anthropic.rs |   6 
crates/language_models/src/provider/google.rs    |   6 
15 files changed, 236 insertions(+), 89 deletions(-)

Detailed changes

crates/acp_tools/src/acp_tools.rs 🔗

@@ -46,7 +46,7 @@ pub struct AcpConnectionRegistry {
 }
 
 struct ActiveConnection {
-    server_name: &'static str,
+    server_name: SharedString,
     connection: Weak<acp::ClientSideConnection>,
 }
 
@@ -63,12 +63,12 @@ impl AcpConnectionRegistry {
 
     pub fn set_active_connection(
         &self,
-        server_name: &'static str,
+        server_name: impl Into<SharedString>,
         connection: &Rc<acp::ClientSideConnection>,
         cx: &mut Context<Self>,
     ) {
         self.active_connection.replace(Some(ActiveConnection {
-            server_name,
+            server_name: server_name.into(),
             connection: Rc::downgrade(connection),
         }));
         cx.notify();
@@ -85,7 +85,7 @@ struct AcpTools {
 }
 
 struct WatchedConnection {
-    server_name: &'static str,
+    server_name: SharedString,
     messages: Vec<WatchedConnectionMessage>,
     list_state: ListState,
     connection: Weak<acp::ClientSideConnection>,
@@ -142,7 +142,7 @@ impl AcpTools {
             });
 
             self.watched_connection = Some(WatchedConnection {
-                server_name: active_connection.server_name,
+                server_name: active_connection.server_name.clone(),
                 messages: vec![],
                 list_state: ListState::new(0, ListAlignment::Bottom, px(2048.)),
                 connection: active_connection.connection.clone(),
@@ -442,7 +442,7 @@ impl Item for AcpTools {
             "ACP: {}",
             self.watched_connection
                 .as_ref()
-                .map_or("Disconnected", |connection| connection.server_name)
+                .map_or("Disconnected", |connection| &connection.server_name)
         )
         .into()
     }

crates/agent2/src/native_agent_server.rs 🔗

@@ -3,7 +3,7 @@ use std::{any::Any, path::Path, rc::Rc, sync::Arc};
 use agent_servers::AgentServer;
 use anyhow::Result;
 use fs::Fs;
-use gpui::{App, Entity, Task};
+use gpui::{App, Entity, SharedString, Task};
 use project::Project;
 use prompt_store::PromptStore;
 
@@ -22,16 +22,16 @@ impl NativeAgentServer {
 }
 
 impl AgentServer for NativeAgentServer {
-    fn name(&self) -> &'static str {
-        "Zed Agent"
+    fn name(&self) -> SharedString {
+        "Zed Agent".into()
     }
 
-    fn empty_state_headline(&self) -> &'static str {
+    fn empty_state_headline(&self) -> SharedString {
         self.name()
     }
 
-    fn empty_state_message(&self) -> &'static str {
-        ""
+    fn empty_state_message(&self) -> SharedString {
+        "".into()
     }
 
     fn logo(&self) -> ui::IconName {

crates/agent_servers/src/acp.rs 🔗

@@ -15,7 +15,7 @@ use std::{path::Path, rc::Rc};
 use thiserror::Error;
 
 use anyhow::{Context as _, Result};
-use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
+use gpui::{App, AppContext as _, AsyncApp, Entity, SharedString, Task, WeakEntity};
 
 use acp_thread::{AcpThread, AuthRequired, LoadError};
 
@@ -24,7 +24,7 @@ use acp_thread::{AcpThread, AuthRequired, LoadError};
 pub struct UnsupportedVersion;
 
 pub struct AcpConnection {
-    server_name: &'static str,
+    server_name: SharedString,
     connection: Rc<acp::ClientSideConnection>,
     sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
     auth_methods: Vec<acp::AuthMethod>,
@@ -38,7 +38,7 @@ pub struct AcpSession {
 }
 
 pub async fn connect(
-    server_name: &'static str,
+    server_name: SharedString,
     command: AgentServerCommand,
     root_dir: &Path,
     cx: &mut AsyncApp,
@@ -51,7 +51,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
 
 impl AcpConnection {
     pub async fn stdio(
-        server_name: &'static str,
+        server_name: SharedString,
         command: AgentServerCommand,
         root_dir: &Path,
         cx: &mut AsyncApp,
@@ -121,7 +121,7 @@ impl AcpConnection {
 
         cx.update(|cx| {
             AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
-                registry.set_active_connection(server_name, &connection, cx)
+                registry.set_active_connection(server_name.clone(), &connection, cx)
             });
         })?;
 
@@ -187,7 +187,7 @@ impl AgentConnection for AcpConnection {
             let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
             let thread = cx.new(|_cx| {
                 AcpThread::new(
-                    self.server_name,
+                    self.server_name.clone(),
                     self.clone(),
                     project,
                     action_log,

crates/agent_servers/src/agent_servers.rs 🔗

@@ -1,5 +1,6 @@
 mod acp;
 mod claude;
+mod custom;
 mod gemini;
 mod settings;
 
@@ -7,6 +8,7 @@ mod settings;
 pub mod e2e_tests;
 
 pub use claude::*;
+pub use custom::*;
 pub use gemini::*;
 pub use settings::*;
 
@@ -31,9 +33,9 @@ pub fn init(cx: &mut App) {
 
 pub trait AgentServer: Send {
     fn logo(&self) -> ui::IconName;
-    fn name(&self) -> &'static str;
-    fn empty_state_headline(&self) -> &'static str;
-    fn empty_state_message(&self) -> &'static str;
+    fn name(&self) -> SharedString;
+    fn empty_state_headline(&self) -> SharedString;
+    fn empty_state_message(&self) -> SharedString;
 
     fn connect(
         &self,

crates/agent_servers/src/claude.rs 🔗

@@ -30,7 +30,7 @@ use futures::{
     io::BufReader,
     select_biased,
 };
-use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
+use gpui::{App, AppContext, AsyncApp, Entity, SharedString, Task, WeakEntity};
 use serde::{Deserialize, Serialize};
 use util::{ResultExt, debug_panic};
 
@@ -43,16 +43,16 @@ use acp_thread::{AcpThread, AgentConnection, AuthRequired, LoadError, MentionUri
 pub struct ClaudeCode;
 
 impl AgentServer for ClaudeCode {
-    fn name(&self) -> &'static str {
-        "Claude Code"
+    fn name(&self) -> SharedString {
+        "Claude Code".into()
     }
 
-    fn empty_state_headline(&self) -> &'static str {
+    fn empty_state_headline(&self) -> SharedString {
         self.name()
     }
 
-    fn empty_state_message(&self) -> &'static str {
-        "How can I help you today?"
+    fn empty_state_message(&self) -> SharedString {
+        "How can I help you today?".into()
     }
 
     fn logo(&self) -> ui::IconName {

crates/agent_servers/src/custom.rs 🔗

@@ -0,0 +1,59 @@
+use crate::{AgentServerCommand, AgentServerSettings};
+use acp_thread::AgentConnection;
+use anyhow::Result;
+use gpui::{App, Entity, SharedString, Task};
+use project::Project;
+use std::{path::Path, rc::Rc};
+use ui::IconName;
+
+/// A generic agent server implementation for custom user-defined agents
+pub struct CustomAgentServer {
+    name: SharedString,
+    command: AgentServerCommand,
+}
+
+impl CustomAgentServer {
+    pub fn new(name: SharedString, settings: &AgentServerSettings) -> Self {
+        Self {
+            name,
+            command: settings.command.clone(),
+        }
+    }
+}
+
+impl crate::AgentServer for CustomAgentServer {
+    fn name(&self) -> SharedString {
+        self.name.clone()
+    }
+
+    fn logo(&self) -> IconName {
+        IconName::Terminal
+    }
+
+    fn empty_state_headline(&self) -> SharedString {
+        "No conversations yet".into()
+    }
+
+    fn empty_state_message(&self) -> SharedString {
+        format!("Start a conversation with {}", self.name).into()
+    }
+
+    fn connect(
+        &self,
+        root_dir: &Path,
+        _project: &Entity<Project>,
+        cx: &mut App,
+    ) -> Task<Result<Rc<dyn AgentConnection>>> {
+        let server_name = self.name();
+        let command = self.command.clone();
+        let root_dir = root_dir.to_path_buf();
+
+        cx.spawn(async move |mut cx| {
+            crate::acp::connect(server_name, command, &root_dir, &mut cx).await
+        })
+    }
+
+    fn into_any(self: Rc<Self>) -> Rc<dyn std::any::Any> {
+        self
+    }
+}

crates/agent_servers/src/e2e_tests.rs 🔗

@@ -1,17 +1,15 @@
-use std::{
-    path::{Path, PathBuf},
-    sync::Arc,
-    time::Duration,
-};
-
 use crate::AgentServer;
 use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
 use agent_client_protocol as acp;
-
 use futures::{FutureExt, StreamExt, channel::mpsc, select};
 use gpui::{AppContext, Entity, TestAppContext};
 use indoc::indoc;
 use project::{FakeFs, Project};
+use std::{
+    path::{Path, PathBuf},
+    sync::Arc,
+    time::Duration,
+};
 use util::path;
 
 pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext)
@@ -479,6 +477,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
                 gemini: Some(crate::AgentServerSettings {
                     command: crate::gemini::tests::local_command(),
                 }),
+                custom: collections::HashMap::default(),
             },
             cx,
         );

crates/agent_servers/src/gemini.rs 🔗

@@ -4,11 +4,10 @@ use std::{any::Any, path::Path};
 use crate::{AgentServer, AgentServerCommand};
 use acp_thread::{AgentConnection, LoadError};
 use anyhow::Result;
-use gpui::{Entity, Task};
+use gpui::{App, Entity, SharedString, Task};
 use language_models::provider::google::GoogleLanguageModelProvider;
 use project::Project;
 use settings::SettingsStore;
-use ui::App;
 
 use crate::AllAgentServersSettings;
 
@@ -18,16 +17,16 @@ pub struct Gemini;
 const ACP_ARG: &str = "--experimental-acp";
 
 impl AgentServer for Gemini {
-    fn name(&self) -> &'static str {
-        "Gemini CLI"
+    fn name(&self) -> SharedString {
+        "Gemini CLI".into()
     }
 
-    fn empty_state_headline(&self) -> &'static str {
+    fn empty_state_headline(&self) -> SharedString {
         self.name()
     }
 
-    fn empty_state_message(&self) -> &'static str {
-        "Ask questions, edit files, run commands"
+    fn empty_state_message(&self) -> SharedString {
+        "Ask questions, edit files, run commands".into()
     }
 
     fn logo(&self) -> ui::IconName {

crates/agent_servers/src/settings.rs 🔗

@@ -1,6 +1,7 @@
 use crate::AgentServerCommand;
 use anyhow::Result;
-use gpui::App;
+use collections::HashMap;
+use gpui::{App, SharedString};
 use schemars::JsonSchema;
 use serde::{Deserialize, Serialize};
 use settings::{Settings, SettingsSources};
@@ -13,9 +14,13 @@ pub fn init(cx: &mut App) {
 pub struct AllAgentServersSettings {
     pub gemini: Option<AgentServerSettings>,
     pub claude: Option<AgentServerSettings>,
+
+    /// Custom agent servers configured by the user
+    #[serde(flatten)]
+    pub custom: HashMap<SharedString, AgentServerSettings>,
 }
 
-#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug)]
+#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
 pub struct AgentServerSettings {
     #[serde(flatten)]
     pub command: AgentServerCommand,
@@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings {
     fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
         let mut settings = AllAgentServersSettings::default();
 
-        for AllAgentServersSettings { gemini, claude } in sources.defaults_and_customizations() {
+        for AllAgentServersSettings {
+            gemini,
+            claude,
+            custom,
+        } in sources.defaults_and_customizations()
+        {
             if gemini.is_some() {
                 settings.gemini = gemini.clone();
             }
             if claude.is_some() {
                 settings.claude = claude.clone();
             }
+
+            // Merge custom agents
+            for (name, config) in custom {
+                // Skip built-in agent names to avoid conflicts
+                if name != "gemini" && name != "claude" {
+                    settings.custom.insert(name.clone(), config.clone());
+                }
+            }
         }
 
         Ok(settings)

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -600,7 +600,7 @@ impl AcpThreadView {
 
             let view = registry.read(cx).provider(&provider_id).map(|provider| {
                 provider.configuration_view(
-                    language_model::ConfigurationViewTargetAgent::Other(agent_name),
+                    language_model::ConfigurationViewTargetAgent::Other(agent_name.clone()),
                     window,
                     cx,
                 )
@@ -1372,7 +1372,7 @@ impl AcpThreadView {
                                                     .icon_color(Color::Muted)
                                                     .style(ButtonStyle::Transparent)
                                                     .tooltip(move |_window, cx| {
-                                                        cx.new(|_| UnavailableEditingTooltip::new(agent_name.into()))
+                                                        cx.new(|_| UnavailableEditingTooltip::new(agent_name.clone()))
                                                             .into()
                                                     })
                                             )
@@ -3911,13 +3911,13 @@ impl AcpThreadView {
         match AgentSettings::get_global(cx).notify_when_agent_waiting {
             NotifyWhenAgentWaiting::PrimaryScreen => {
                 if let Some(primary) = cx.primary_display() {
-                    self.pop_up(icon, caption.into(), title.into(), window, primary, cx);
+                    self.pop_up(icon, caption.into(), title, window, primary, cx);
                 }
             }
             NotifyWhenAgentWaiting::AllScreens => {
                 let caption = caption.into();
                 for screen in cx.displays() {
-                    self.pop_up(icon, caption.clone(), title.into(), window, screen, cx);
+                    self.pop_up(icon, caption.clone(), title.clone(), window, screen, cx);
                 }
             }
             NotifyWhenAgentWaiting::Never => {
@@ -5153,16 +5153,16 @@ pub(crate) mod tests {
             ui::IconName::Ai
         }
 
-        fn name(&self) -> &'static str {
-            "Test"
+        fn name(&self) -> SharedString {
+            "Test".into()
         }
 
-        fn empty_state_headline(&self) -> &'static str {
-            "Test"
+        fn empty_state_headline(&self) -> SharedString {
+            "Test".into()
         }
 
-        fn empty_state_message(&self) -> &'static str {
-            "Test"
+        fn empty_state_message(&self) -> SharedString {
+            "Test".into()
         }
 
         fn connect(

crates/agent_ui/src/agent_panel.rs 🔗

@@ -5,6 +5,7 @@ use std::sync::Arc;
 use std::time::Duration;
 
 use acp_thread::AcpThread;
+use agent_servers::AgentServerSettings;
 use agent2::{DbThreadMetadata, HistoryEntry};
 use db::kvp::{Dismissable, KEY_VALUE_STORE};
 use serde::{Deserialize, Serialize};
@@ -128,7 +129,7 @@ pub fn init(cx: &mut App) {
                     if let Some(panel) = workspace.panel::<AgentPanel>(cx) {
                         workspace.focus_panel::<AgentPanel>(window, cx);
                         panel.update(cx, |panel, cx| {
-                            panel.external_thread(action.agent, None, None, window, cx)
+                            panel.external_thread(action.agent.clone(), None, None, window, cx)
                         });
                     }
                 })
@@ -239,7 +240,7 @@ enum WhichFontSize {
     None,
 }
 
-#[derive(Debug, Default, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
+#[derive(Debug, Default, Clone, PartialEq, Serialize, Deserialize)]
 pub enum AgentType {
     #[default]
     Zed,
@@ -247,23 +248,29 @@ pub enum AgentType {
     Gemini,
     ClaudeCode,
     NativeAgent,
+    Custom {
+        name: SharedString,
+        settings: AgentServerSettings,
+    },
 }
 
 impl AgentType {
-    fn label(self) -> impl Into<SharedString> {
+    fn label(&self) -> SharedString {
         match self {
-            Self::Zed | Self::TextThread => "Zed Agent",
-            Self::NativeAgent => "Agent 2",
-            Self::Gemini => "Gemini CLI",
-            Self::ClaudeCode => "Claude Code",
+            Self::Zed | Self::TextThread => "Zed Agent".into(),
+            Self::NativeAgent => "Agent 2".into(),
+            Self::Gemini => "Gemini CLI".into(),
+            Self::ClaudeCode => "Claude Code".into(),
+            Self::Custom { name, .. } => name.into(),
         }
     }
 
-    fn icon(self) -> Option<IconName> {
+    fn icon(&self) -> Option<IconName> {
         match self {
             Self::Zed | Self::NativeAgent | Self::TextThread => None,
             Self::Gemini => Some(IconName::AiGemini),
             Self::ClaudeCode => Some(IconName::AiClaude),
+            Self::Custom { .. } => Some(IconName::Terminal),
         }
     }
 }
@@ -517,7 +524,7 @@ pub struct AgentPanel {
 impl AgentPanel {
     fn serialize(&mut self, cx: &mut Context<Self>) {
         let width = self.width;
-        let selected_agent = self.selected_agent;
+        let selected_agent = self.selected_agent.clone();
         self.pending_serialization = Some(cx.background_spawn(async move {
             KEY_VALUE_STORE
                 .write_kvp(
@@ -607,7 +614,7 @@ impl AgentPanel {
                     panel.update(cx, |panel, cx| {
                         panel.width = serialized_panel.width.map(|w| w.round());
                         if let Some(selected_agent) = serialized_panel.selected_agent {
-                            panel.selected_agent = selected_agent;
+                            panel.selected_agent = selected_agent.clone();
                             panel.new_agent_thread(selected_agent, window, cx);
                         }
                         cx.notify();
@@ -1077,14 +1084,17 @@ impl AgentPanel {
         cx.spawn_in(window, async move |this, cx| {
             let ext_agent = match agent_choice {
                 Some(agent) => {
-                    cx.background_spawn(async move {
-                        if let Some(serialized) =
-                            serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
-                        {
-                            KEY_VALUE_STORE
-                                .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
-                                .await
-                                .log_err();
+                    cx.background_spawn({
+                        let agent = agent.clone();
+                        async move {
+                            if let Some(serialized) =
+                                serde_json::to_string(&LastUsedExternalAgent { agent }).log_err()
+                            {
+                                KEY_VALUE_STORE
+                                    .write_kvp(LAST_USED_EXTERNAL_AGENT_KEY.to_string(), serialized)
+                                    .await
+                                    .log_err();
+                            }
                         }
                     })
                     .detach();
@@ -1110,7 +1120,9 @@ impl AgentPanel {
 
             this.update_in(cx, |this, window, cx| {
                 match ext_agent {
-                    crate::ExternalAgent::Gemini | crate::ExternalAgent::NativeAgent => {
+                    crate::ExternalAgent::Gemini
+                    | crate::ExternalAgent::NativeAgent
+                    | crate::ExternalAgent::Custom { .. } => {
                         if !cx.has_flag::<GeminiAndNativeFeatureFlag>() {
                             return;
                         }
@@ -1839,14 +1851,14 @@ impl AgentPanel {
         cx: &mut Context<Self>,
     ) {
         if self.selected_agent != agent {
-            self.selected_agent = agent;
+            self.selected_agent = agent.clone();
             self.serialize(cx);
         }
         self.new_agent_thread(agent, window, cx);
     }
 
     pub fn selected_agent(&self) -> AgentType {
-        self.selected_agent
+        self.selected_agent.clone()
     }
 
     pub fn new_agent_thread(
@@ -1885,6 +1897,13 @@ impl AgentPanel {
                 window,
                 cx,
             ),
+            AgentType::Custom { name, settings } => self.external_thread(
+                Some(crate::ExternalAgent::Custom { name, settings }),
+                None,
+                None,
+                window,
+                cx,
+            ),
         }
     }
 
@@ -2610,13 +2629,55 @@ impl AgentPanel {
                                             }
                                         }),
                                 )
+                            })
+                            .when(cx.has_flag::<GeminiAndNativeFeatureFlag>(), |mut menu| {
+                                // Add custom agents from settings
+                                let settings =
+                                    agent_servers::AllAgentServersSettings::get_global(cx);
+                                for (agent_name, agent_settings) in &settings.custom {
+                                    menu = menu.item(
+                                        ContextMenuEntry::new(format!("New {} Thread", agent_name))
+                                            .icon(IconName::Terminal)
+                                            .icon_color(Color::Muted)
+                                            .handler({
+                                                let workspace = workspace.clone();
+                                                let agent_name = agent_name.clone();
+                                                let agent_settings = agent_settings.clone();
+                                                move |window, cx| {
+                                                    if let Some(workspace) = workspace.upgrade() {
+                                                        workspace.update(cx, |workspace, cx| {
+                                                            if let Some(panel) =
+                                                                workspace.panel::<AgentPanel>(cx)
+                                                            {
+                                                                panel.update(cx, |panel, cx| {
+                                                                    panel.set_selected_agent(
+                                                                        AgentType::Custom {
+                                                                            name: agent_name
+                                                                                .clone(),
+                                                                            settings:
+                                                                                agent_settings
+                                                                                    .clone(),
+                                                                        },
+                                                                        window,
+                                                                        cx,
+                                                                    );
+                                                                });
+                                                            }
+                                                        });
+                                                    }
+                                                }
+                                            }),
+                                    );
+                                }
+
+                                menu
                             });
                         menu
                     }))
                 }
             });
 
-        let selected_agent_label = self.selected_agent.label().into();
+        let selected_agent_label = self.selected_agent.label();
         let selected_agent = div()
             .id("selected_agent_icon")
             .when_some(self.selected_agent.icon(), |this, icon| {

crates/agent_ui/src/agent_ui.rs 🔗

@@ -28,13 +28,14 @@ use std::rc::Rc;
 use std::sync::Arc;
 
 use agent::{Thread, ThreadId};
+use agent_servers::AgentServerSettings;
 use agent_settings::{AgentProfileId, AgentSettings, LanguageModelSelection};
 use assistant_slash_command::SlashCommandRegistry;
 use client::Client;
 use command_palette_hooks::CommandPaletteFilter;
 use feature_flags::FeatureFlagAppExt as _;
 use fs::Fs;
-use gpui::{Action, App, Entity, actions};
+use gpui::{Action, App, Entity, SharedString, actions};
 use language::LanguageRegistry;
 use language_model::{
     ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
@@ -159,13 +160,17 @@ pub struct NewNativeAgentThreadFromSummary {
     from_session_id: agent_client_protocol::SessionId,
 }
 
-#[derive(Default, Debug, Clone, Copy, PartialEq, Serialize, Deserialize, JsonSchema)]
+#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize, JsonSchema)]
 #[serde(rename_all = "snake_case")]
 enum ExternalAgent {
     #[default]
     Gemini,
     ClaudeCode,
     NativeAgent,
+    Custom {
+        name: SharedString,
+        settings: AgentServerSettings,
+    },
 }
 
 impl ExternalAgent {
@@ -175,9 +180,13 @@ impl ExternalAgent {
         history: Entity<agent2::HistoryStore>,
     ) -> Rc<dyn agent_servers::AgentServer> {
         match self {
-            ExternalAgent::Gemini => Rc::new(agent_servers::Gemini),
-            ExternalAgent::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
-            ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
+            Self::Gemini => Rc::new(agent_servers::Gemini),
+            Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
+            Self::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs, history)),
+            Self::Custom { name, settings } => Rc::new(agent_servers::CustomAgentServer::new(
+                name.clone(),
+                settings,
+            )),
         }
     }
 }

crates/language_model/src/language_model.rs 🔗

@@ -643,11 +643,11 @@ pub trait LanguageModelProvider: 'static {
     fn reset_credentials(&self, cx: &mut App) -> Task<Result<()>>;
 }
 
-#[derive(Default, Clone, Copy)]
+#[derive(Default, Clone)]
 pub enum ConfigurationViewTargetAgent {
     #[default]
     ZedAgent,
-    Other(&'static str),
+    Other(SharedString),
 }
 
 #[derive(PartialEq, Eq)]

crates/language_models/src/provider/anthropic.rs 🔗

@@ -1041,9 +1041,9 @@ impl Render for ConfigurationView {
             v_flex()
                 .size_full()
                 .on_action(cx.listener(Self::save_api_key))
-                .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent {
-                    ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic",
-                    ConfigurationViewTargetAgent::Other(agent) => agent,
+                .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
+                    ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Anthropic".into(),
+                    ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
                 })))
                 .child(
                     List::new()

crates/language_models/src/provider/google.rs 🔗

@@ -921,9 +921,9 @@ impl Render for ConfigurationView {
             v_flex()
                 .size_full()
                 .on_action(cx.listener(Self::save_api_key))
-                .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match self.target_agent {
-                    ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI",
-                    ConfigurationViewTargetAgent::Other(agent) => agent,
+                .child(Label::new(format!("To use {}, you need to add an API key. Follow these steps:", match &self.target_agent {
+                    ConfigurationViewTargetAgent::ZedAgent => "Zed's agent with Google AI".into(),
+                    ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
                 })))
                 .child(
                     List::new()