Detailed changes
@@ -44,19 +44,27 @@ impl crate::AgentServer for CustomAgentServer {
settings
.as_ref()
- .and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
+ .and_then(|s| s.default_mode().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
- if let Some(settings) = settings
+ let settings = settings
.agent_servers
.get_or_insert_default()
.custom
- .get_mut(&name)
- {
- settings.default_mode = mode_id.map(|m| m.to_string())
+ .entry(name.clone())
+ .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
+ default_model: None,
+ default_mode: None,
+ });
+
+ match settings {
+ settings::CustomAgentServerSettings::Custom { default_mode, .. }
+ | settings::CustomAgentServerSettings::Extension { default_mode, .. } => {
+ *default_mode = mode_id.map(|m| m.to_string());
+ }
}
});
}
@@ -72,19 +80,27 @@ impl crate::AgentServer for CustomAgentServer {
settings
.as_ref()
- .and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
+ .and_then(|s| s.default_model().map(|m| acp::ModelId(m.into())))
}
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file(fs, cx, move |settings, _| {
- if let Some(settings) = settings
+ let settings = settings
.agent_servers
.get_or_insert_default()
.custom
- .get_mut(&name)
- {
- settings.default_model = model_id.map(|m| m.to_string())
+ .entry(name.clone())
+ .or_insert_with(|| settings::CustomAgentServerSettings::Extension {
+ default_model: None,
+ default_mode: None,
+ });
+
+ match settings {
+ settings::CustomAgentServerSettings::Custom { default_model, .. }
+ | settings::CustomAgentServerSettings::Extension { default_model, .. } => {
+ *default_model = model_id.map(|m| m.to_string());
+ }
}
});
}
@@ -1343,7 +1343,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
.custom
.insert(
server_name,
- settings::CustomAgentServerSettings {
+ settings::CustomAgentServerSettings::Custom {
path: "path_to_executable".into(),
args: vec![],
env: Some(HashMap::default()),
@@ -8,9 +8,7 @@ use agent::{ContextServerRegistry, DbThreadMetadata, HistoryEntry, HistoryStore}
use db::kvp::{Dismissable, KEY_VALUE_STORE};
use project::{
ExternalAgentServerName,
- agent_server_store::{
- AgentServerCommand, AllAgentServersSettings, CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME,
- },
+ agent_server_store::{CLAUDE_CODE_NAME, CODEX_NAME, GEMINI_NAME},
};
use serde::{Deserialize, Serialize};
use settings::{
@@ -35,9 +33,7 @@ use crate::{
ExpandMessageEditor,
acp::{AcpThreadHistory, ThreadHistoryEvent},
};
-use crate::{
- ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary, placeholder_command,
-};
+use crate::{ExternalAgent, NewExternalAgentThread, NewNativeAgentThreadFromSummary};
use crate::{ManageProfiles, context_store::ContextStore};
use agent_settings::AgentSettings;
use ai_onboarding::AgentPanelOnboarding;
@@ -61,7 +57,7 @@ use project::{Project, ProjectPath, Worktree};
use prompt_store::{PromptBuilder, PromptStore, UserPromptId};
use rules_library::{RulesLibrary, open_rules_library};
use search::{BufferSearchBar, buffer_search};
-use settings::{Settings, SettingsStore, update_settings_file};
+use settings::{Settings, update_settings_file};
use theme::ThemeSettings;
use ui::utils::WithRemSize;
use ui::{
@@ -248,7 +244,6 @@ pub enum AgentType {
Codex,
Custom {
name: SharedString,
- command: AgentServerCommand,
},
}
@@ -280,7 +275,7 @@ impl From<ExternalAgent> for AgentType {
ExternalAgent::Gemini => Self::Gemini,
ExternalAgent::ClaudeCode => Self::ClaudeCode,
ExternalAgent::Codex => Self::Codex,
- ExternalAgent::Custom { name, command } => Self::Custom { name, command },
+ ExternalAgent::Custom { name } => Self::Custom { name },
ExternalAgent::NativeAgent => Self::NativeAgent,
}
}
@@ -1459,8 +1454,8 @@ impl AgentPanel {
self.serialize(cx);
self.external_thread(Some(crate::ExternalAgent::Codex), None, None, window, cx)
}
- AgentType::Custom { name, command } => self.external_thread(
- Some(crate::ExternalAgent::Custom { name, command }),
+ AgentType::Custom { name } => self.external_thread(
+ Some(crate::ExternalAgent::Custom { name }),
None,
None,
window,
@@ -2085,22 +2080,11 @@ impl AgentPanel {
.cloned()
.collect::<Vec<_>>();
- let custom_settings = cx
- .global::<SettingsStore>()
- .get::<AllAgentServersSettings>(None)
- .custom
- .clone();
-
for agent_name in agent_names {
let icon_path = agent_server_store.agent_icon(&agent_name);
let mut entry = ContextMenuEntry::new(agent_name.clone());
- let command = custom_settings
- .get(&agent_name.0)
- .map(|settings| settings.command.clone())
- .unwrap_or(placeholder_command());
-
if let Some(icon_path) = icon_path {
entry = entry.custom_icon_svg(icon_path);
} else {
@@ -2110,7 +2094,6 @@ impl AgentPanel {
.when(
is_agent_selected(AgentType::Custom {
name: agent_name.0.clone(),
- command: command.clone(),
}),
|this| {
this.action(Box::new(NewExternalAgentThread { agent: None }))
@@ -2121,7 +2104,6 @@ impl AgentPanel {
.handler({
let workspace = workspace.clone();
let agent_name = agent_name.clone();
- let custom_settings = custom_settings.clone();
move |window, cx| {
if let Some(workspace) = workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
@@ -2134,17 +2116,6 @@ impl AgentPanel {
name: agent_name
.clone()
.into(),
- command: custom_settings
- .get(&agent_name.0)
- .map(|settings| {
- settings
- .command
- .clone()
- })
- .unwrap_or(
- placeholder_command(
- ),
- ),
},
window,
cx,
@@ -38,7 +38,6 @@ use language_model::{
ConfiguredModel, LanguageModel, LanguageModelId, LanguageModelProviderId, LanguageModelRegistry,
};
use project::DisableAiSettings;
-use project::agent_server_store::AgentServerCommand;
use prompt_store::PromptBuilder;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -162,18 +161,7 @@ pub enum ExternalAgent {
ClaudeCode,
Codex,
NativeAgent,
- Custom {
- name: SharedString,
- command: AgentServerCommand,
- },
-}
-
-fn placeholder_command() -> AgentServerCommand {
- AgentServerCommand {
- path: "/placeholder".into(),
- args: vec![],
- env: None,
- }
+ Custom { name: SharedString },
}
impl ExternalAgent {
@@ -197,9 +185,7 @@ impl ExternalAgent {
Self::ClaudeCode => Rc::new(agent_servers::ClaudeCode),
Self::Codex => Rc::new(agent_servers::Codex),
Self::NativeAgent => Rc::new(agent::NativeAgentServer::new(fs, history)),
- Self::Custom { name, command: _ } => {
- Rc::new(agent_servers::CustomAgentServer::new(name.clone()))
- }
+ Self::Custom { name } => Rc::new(agent_servers::CustomAgentServer::new(name.clone())),
}
}
}
@@ -141,3 +141,9 @@ pub(crate) mod m_2025_11_12 {
pub(crate) use settings::SETTINGS_PATTERNS;
}
+
+pub(crate) mod m_2025_11_20 {
+ mod settings;
+
+ pub(crate) use settings::SETTINGS_PATTERNS;
+}
@@ -0,0 +1,76 @@
+use std::ops::Range;
+
+use tree_sitter::{Query, QueryMatch};
+
+use crate::MigrationPatterns;
+
+pub const SETTINGS_PATTERNS: MigrationPatterns = &[(
+ SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN,
+ migrate_custom_agent_settings,
+)];
+
+const SETTINGS_AGENT_SERVERS_CUSTOM_PATTERN: &str = r#"(document
+ (object
+ (pair
+ key: (string (string_content) @agent-servers)
+ value: (object
+ (pair
+ key: (string (string_content) @server-name)
+ value: (object) @server-settings
+ )
+ )
+ )
+ )
+ (#eq? @agent-servers "agent_servers")
+)"#;
+
+fn migrate_custom_agent_settings(
+ contents: &str,
+ mat: &QueryMatch,
+ query: &Query,
+) -> Option<(Range<usize>, String)> {
+ let server_name_index = query.capture_index_for_name("server-name")?;
+ let server_name = mat.nodes_for_capture_index(server_name_index).next()?;
+ let server_name_text = &contents[server_name.byte_range()];
+
+ if matches!(server_name_text, "gemini" | "claude" | "codex") {
+ return None;
+ }
+
+ let server_settings_index = query.capture_index_for_name("server-settings")?;
+ let server_settings = mat.nodes_for_capture_index(server_settings_index).next()?;
+
+ let mut column = None;
+
+ // Parse the server settings to check what keys it contains
+ let mut cursor = server_settings.walk();
+ for child in server_settings.children(&mut cursor) {
+ if child.kind() == "pair" {
+ if let Some(key_node) = child.child_by_field_name("key") {
+ if let (None, Some(quote_content)) = (column, key_node.child(0)) {
+ column = Some(quote_content.start_position().column);
+ }
+ if let Some(string_content) = key_node.child(1) {
+ let key = &contents[string_content.byte_range()];
+ match key {
+ // If it already has a type key, don't modify it
+ "type" => return None,
+ _ => {}
+ }
+ }
+ }
+ }
+ }
+
+ // Insert the type key at the beginning of the object
+ let start = server_settings.start_byte() + 1;
+ let indent = " ".repeat(column.unwrap_or(12));
+
+ Some((
+ start..start,
+ format!(
+ r#"
+{indent}"type": "custom","#
+ ),
+ ))
+}
@@ -219,6 +219,10 @@ pub fn migrate_settings(text: &str) -> Result<Option<String>> {
migrations::m_2025_11_12::SETTINGS_PATTERNS,
&SETTINGS_QUERY_2025_11_12,
),
+ MigrationType::TreeSitter(
+ migrations::m_2025_11_20::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_11_20,
+ ),
];
run_migrations(text, migrations)
}
@@ -341,6 +345,10 @@ define_query!(
SETTINGS_QUERY_2025_11_12,
migrations::m_2025_11_12::SETTINGS_PATTERNS
);
+define_query!(
+ SETTINGS_QUERY_2025_11_20,
+ migrations::m_2025_11_20::SETTINGS_PATTERNS
+);
// custom query
static EDIT_PREDICTION_SETTINGS_MIGRATION_QUERY: LazyLock<Query> = LazyLock::new(|| {
@@ -1192,6 +1200,63 @@ mod tests {
);
}
+ #[test]
+ fn test_custom_agent_server_settings_migration() {
+ assert_migrate_settings_with_migrations(
+ &[MigrationType::TreeSitter(
+ migrations::m_2025_11_20::SETTINGS_PATTERNS,
+ &SETTINGS_QUERY_2025_11_20,
+ )],
+ r#"{
+ "agent_servers": {
+ "gemini": {
+ "default_model": "gemini-1.5-pro"
+ },
+ "claude": {},
+ "codex": {},
+ "my-custom-agent": {
+ "command": "/path/to/agent",
+ "args": ["--foo"],
+ "default_model": "my-model"
+ },
+ "already-migrated-agent": {
+ "type": "custom",
+ "command": "/path/to/agent"
+ },
+ "future-extension-agent": {
+ "type": "extension",
+ "default_model": "ext-model"
+ }
+ }
+}"#,
+ Some(
+ r#"{
+ "agent_servers": {
+ "gemini": {
+ "default_model": "gemini-1.5-pro"
+ },
+ "claude": {},
+ "codex": {},
+ "my-custom-agent": {
+ "type": "custom",
+ "command": "/path/to/agent",
+ "args": ["--foo"],
+ "default_model": "my-model"
+ },
+ "already-migrated-agent": {
+ "type": "custom",
+ "command": "/path/to/agent"
+ },
+ "future-extension-agent": {
+ "type": "extension",
+ "default_model": "ext-model"
+ }
+ }
+}"#,
+ ),
+ );
+ }
+
#[test]
fn test_remove_version_fields() {
assert_migrate_settings(
@@ -469,15 +469,21 @@ impl AgentServerStore {
}),
);
self.external_agents
- .extend(new_settings.custom.iter().map(|(name, settings)| {
- (
- ExternalAgentServerName(name.clone()),
- Box::new(LocalCustomAgent {
- command: settings.command.clone(),
- project_environment: project_environment.clone(),
- }) as Box<dyn ExternalAgentServer>,
- )
- }));
+ .extend(
+ new_settings
+ .custom
+ .iter()
+ .filter_map(|(name, settings)| match settings {
+ CustomAgentServerSettings::Custom { command, .. } => Some((
+ ExternalAgentServerName(name.clone()),
+ Box::new(LocalCustomAgent {
+ command: command.clone(),
+ project_environment: project_environment.clone(),
+ }) as Box<dyn ExternalAgentServer>,
+ )),
+ CustomAgentServerSettings::Extension { .. } => None,
+ }),
+ );
self.external_agents.extend(extension_agents.iter().map(
|(agent_name, ext_id, targets, env, icon_path)| {
let name = ExternalAgentServerName(agent_name.clone().into());
@@ -1817,32 +1823,88 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
}
#[derive(Clone, JsonSchema, Debug, PartialEq)]
-pub struct CustomAgentServerSettings {
- pub command: AgentServerCommand,
- /// The default mode to use for this agent.
- ///
- /// Note: Not only all agents support modes.
- ///
- /// Default: None
- pub default_mode: Option<String>,
- /// The default model to use for this agent.
- ///
- /// This should be the model ID as reported by the agent.
- ///
- /// Default: None
- pub default_model: Option<String>,
+pub enum CustomAgentServerSettings {
+ Custom {
+ command: AgentServerCommand,
+ /// The default mode to use for this agent.
+ ///
+ /// Note: Not only all agents support modes.
+ ///
+ /// Default: None
+ default_mode: Option<String>,
+ /// The default model to use for this agent.
+ ///
+ /// This should be the model ID as reported by the agent.
+ ///
+ /// Default: None
+ default_model: Option<String>,
+ },
+ Extension {
+ /// The default mode to use for this agent.
+ ///
+ /// Note: Not only all agents support modes.
+ ///
+ /// Default: None
+ default_mode: Option<String>,
+ /// The default model to use for this agent.
+ ///
+ /// This should be the model ID as reported by the agent.
+ ///
+ /// Default: None
+ default_model: Option<String>,
+ },
+}
+
+impl CustomAgentServerSettings {
+ pub fn command(&self) -> Option<&AgentServerCommand> {
+ match self {
+ CustomAgentServerSettings::Custom { command, .. } => Some(command),
+ CustomAgentServerSettings::Extension { .. } => None,
+ }
+ }
+
+ pub fn default_mode(&self) -> Option<&str> {
+ match self {
+ CustomAgentServerSettings::Custom { default_mode, .. }
+ | CustomAgentServerSettings::Extension { default_mode, .. } => default_mode.as_deref(),
+ }
+ }
+
+ pub fn default_model(&self) -> Option<&str> {
+ match self {
+ CustomAgentServerSettings::Custom { default_model, .. }
+ | CustomAgentServerSettings::Extension { default_model, .. } => {
+ default_model.as_deref()
+ }
+ }
+ }
}
impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
fn from(value: settings::CustomAgentServerSettings) -> Self {
- CustomAgentServerSettings {
- command: AgentServerCommand {
- path: PathBuf::from(shellexpand::tilde(&value.path.to_string_lossy()).as_ref()),
- args: value.args,
- env: value.env,
+ match value {
+ settings::CustomAgentServerSettings::Custom {
+ path,
+ args,
+ env,
+ default_mode,
+ default_model,
+ } => CustomAgentServerSettings::Custom {
+ command: AgentServerCommand {
+ path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
+ args,
+ env,
+ },
+ default_mode,
+ default_model,
+ },
+ settings::CustomAgentServerSettings::Extension {
+ default_mode,
+ default_model,
+ } => CustomAgentServerSettings::Extension {
+ default_mode,
+ default_model,
},
- default_mode: value.default_mode,
- default_model: value.default_model,
}
}
}
@@ -2176,7 +2238,7 @@ mod extension_agent_tests {
"Tilde should be expanded for builtin agent path"
);
- let settings = settings::CustomAgentServerSettings {
+ let settings = settings::CustomAgentServerSettings::Custom {
path: PathBuf::from("~/custom/agent"),
args: vec!["serve".into()],
env: None,
@@ -2184,10 +2246,14 @@ mod extension_agent_tests {
default_model: None,
};
- let CustomAgentServerSettings {
+ let converted: CustomAgentServerSettings = settings.into();
+ let CustomAgentServerSettings::Custom {
command: AgentServerCommand { path, .. },
..
- } = settings.into();
+ } = converted
+ else {
+ panic!("Expected Custom variant");
+ };
assert!(
!path.to_string_lossy().starts_with("~"),
@@ -1838,6 +1838,7 @@ async fn test_remote_external_agent_server(
&json!({
"agent_servers": {
"foo": {
+ "type": "custom",
"command": "foo-cli",
"args": ["--flag"],
"env": {
@@ -342,22 +342,39 @@ pub struct BuiltinAgentServerSettings {
#[skip_serializing_none]
#[derive(Deserialize, Serialize, Clone, JsonSchema, MergeFrom, Debug, PartialEq)]
-pub struct CustomAgentServerSettings {
- #[serde(rename = "command")]
- pub path: PathBuf,
- #[serde(default)]
- pub args: Vec<String>,
- pub env: Option<HashMap<String, String>>,
- /// The default mode to use for this agent.
- ///
- /// Note: Not only all agents support modes.
- ///
- /// Default: None
- pub default_mode: Option<String>,
- /// The default model to use for this agent.
- ///
- /// This should be the model ID as reported by the agent.
- ///
- /// Default: None
- pub default_model: Option<String>,
+#[serde(tag = "type", rename_all = "snake_case")]
+pub enum CustomAgentServerSettings {
+ Custom {
+ #[serde(rename = "command")]
+ path: PathBuf,
+ #[serde(default)]
+ args: Vec<String>,
+ env: Option<HashMap<String, String>>,
+ /// The default mode to use for this agent.
+ ///
+ /// Note: Not only all agents support modes.
+ ///
+ /// Default: None
+ default_mode: Option<String>,
+ /// The default model to use for this agent.
+ ///
+ /// This should be the model ID as reported by the agent.
+ ///
+ /// Default: None
+ default_model: Option<String>,
+ },
+ Extension {
+ /// The default mode to use for this agent.
+ ///
+ /// Note: Not only all agents support modes.
+ ///
+ /// Default: None
+ default_mode: Option<String>,
+ /// The default model to use for this agent.
+ ///
+ /// This should be the model ID as reported by the agent.
+ ///
+ /// Default: None
+ default_model: Option<String>,
+ },
}