acp: Support launching custom agent servers (#36805)

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
This commit is contained in:
Antonio Scandurra 2025-08-23 16:30:54 +02:00 committed by Joseph T. Lyons
parent 7bf6cc058c
commit ad6bc4586a
15 changed files with 238 additions and 91 deletions

View file

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

View file

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

View file

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

View file

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

View file

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

View file

@ -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
}
}

View file

@ -1,17 +1,15 @@
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::{ use std::{
path::{Path, PathBuf}, path::{Path, PathBuf},
sync::Arc, sync::Arc,
time::Duration, 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 util::path; use util::path;
pub async fn test_basic<T, F>(server: F, cx: &mut TestAppContext) 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 { gemini: Some(crate::AgentServerSettings {
command: crate::gemini::tests::local_command(), command: crate::gemini::tests::local_command(),
}), }),
custom: collections::HashMap::default(),
}, },
cx, cx,
); );

View file

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

View file

@ -1,6 +1,7 @@
use crate::AgentServerCommand; use crate::AgentServerCommand;
use anyhow::Result; use anyhow::Result;
use gpui::App; use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema; use schemars::JsonSchema;
use serde::{Deserialize, Serialize}; use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsSources}; use settings::{Settings, SettingsSources};
@ -13,9 +14,13 @@ pub fn init(cx: &mut App) {
pub struct AllAgentServersSettings { pub struct AllAgentServersSettings {
pub gemini: Option<AgentServerSettings>, pub gemini: Option<AgentServerSettings>,
pub claude: 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 { pub struct AgentServerSettings {
#[serde(flatten)] #[serde(flatten)]
pub command: AgentServerCommand, pub command: AgentServerCommand,
@ -29,13 +34,26 @@ impl settings::Settings for AllAgentServersSettings {
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> { fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default(); 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() { if gemini.is_some() {
settings.gemini = gemini.clone(); settings.gemini = gemini.clone();
} }
if claude.is_some() { if claude.is_some() {
settings.claude = claude.clone(); 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) Ok(settings)

View file

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

View file

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

View file

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

View file

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

View file

@ -1041,9 +1041,9 @@ impl Render for ConfigurationView {
v_flex() v_flex()
.size_full() .size_full()
.on_action(cx.listener(Self::save_api_key)) .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 { .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::ZedAgent => "Zed's agent with Anthropic".into(),
ConfigurationViewTargetAgent::Other(agent) => agent, ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
}))) })))
.child( .child(
List::new() List::new()

View file

@ -921,9 +921,9 @@ impl Render for ConfigurationView {
v_flex() v_flex()
.size_full() .size_full()
.on_action(cx.listener(Self::save_api_key)) .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 { .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::ZedAgent => "Zed's agent with Google AI".into(),
ConfigurationViewTargetAgent::Other(agent) => agent, ConfigurationViewTargetAgent::Other(agent) => agent.clone(),
}))) })))
.child( .child(
List::new() List::new()