Detailed changes
@@ -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()
}
@@ -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 {
@@ -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,
@@ -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,
@@ -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 {
@@ -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
+ }
+}
@@ -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,
);
@@ -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 {
@@ -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)
@@ -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(
@@ -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| {
@@ -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,
+ )),
}
}
}
@@ -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)]
@@ -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()
@@ -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()