Skip to content

Commit 28f5097

Browse files
authored
agent_ui: Add support for setting a model as the default for external agents (#43122)
This PR builds on top of the `default_mode` feature where it was possible to set an external agent mode as the default if you held a modifier while clicking on the desired option. Now, if you want to have, for example, Haiku as your default Claude Code model, you can do that. This feature adds parity between external agents and Zed's built-in one, which already supported this feature for a little while. Note: This still doesn't work with external agents installed from extensions. At the moment, this is limited to Claude Code, Codex, and Gemini—the ones we include out of the box. Release Notes: - agent: Added the ability to set a model as the default for a given built-in external agent (Claude Code, Codex CLI, or Gemini CLI).
1 parent 95cb467 commit 28f5097

File tree

16 files changed

+276
-41
lines changed

16 files changed

+276
-41
lines changed

crates/agent_servers/src/acp.rs

Lines changed: 54 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -35,6 +35,7 @@ pub struct AcpConnection {
3535
auth_methods: Vec<acp::AuthMethod>,
3636
agent_capabilities: acp::AgentCapabilities,
3737
default_mode: Option<acp::SessionModeId>,
38+
default_model: Option<acp::ModelId>,
3839
root_dir: PathBuf,
3940
// NB: Don't move this into the wait_task, since we need to ensure the process is
4041
// killed on drop (setting kill_on_drop on the command seems to not always work).
@@ -57,6 +58,7 @@ pub async fn connect(
5758
command: AgentServerCommand,
5859
root_dir: &Path,
5960
default_mode: Option<acp::SessionModeId>,
61+
default_model: Option<acp::ModelId>,
6062
is_remote: bool,
6163
cx: &mut AsyncApp,
6264
) -> Result<Rc<dyn AgentConnection>> {
@@ -66,6 +68,7 @@ pub async fn connect(
6668
command.clone(),
6769
root_dir,
6870
default_mode,
71+
default_model,
6972
is_remote,
7073
cx,
7174
)
@@ -82,6 +85,7 @@ impl AcpConnection {
8285
command: AgentServerCommand,
8386
root_dir: &Path,
8487
default_mode: Option<acp::SessionModeId>,
88+
default_model: Option<acp::ModelId>,
8589
is_remote: bool,
8690
cx: &mut AsyncApp,
8791
) -> Result<Self> {
@@ -207,6 +211,7 @@ impl AcpConnection {
207211
sessions,
208212
agent_capabilities: response.agent_capabilities,
209213
default_mode,
214+
default_model,
210215
_io_task: io_task,
211216
_wait_task: wait_task,
212217
_stderr_task: stderr_task,
@@ -245,6 +250,7 @@ impl AgentConnection for AcpConnection {
245250
let conn = self.connection.clone();
246251
let sessions = self.sessions.clone();
247252
let default_mode = self.default_mode.clone();
253+
let default_model = self.default_model.clone();
248254
let cwd = cwd.to_path_buf();
249255
let context_server_store = project.read(cx).context_server_store().read(cx);
250256
let mcp_servers =
@@ -333,6 +339,7 @@ impl AgentConnection for AcpConnection {
333339
let default_mode = default_mode.clone();
334340
let session_id = response.session_id.clone();
335341
let modes = modes.clone();
342+
let conn = conn.clone();
336343
async move |_| {
337344
let result = conn.set_session_mode(acp::SetSessionModeRequest {
338345
session_id,
@@ -367,6 +374,53 @@ impl AgentConnection for AcpConnection {
367374
}
368375
}
369376

377+
if let Some(default_model) = default_model {
378+
if let Some(models) = models.as_ref() {
379+
let mut models_ref = models.borrow_mut();
380+
let has_model = models_ref.available_models.iter().any(|model| model.model_id == default_model);
381+
382+
if has_model {
383+
let initial_model_id = models_ref.current_model_id.clone();
384+
385+
cx.spawn({
386+
let default_model = default_model.clone();
387+
let session_id = response.session_id.clone();
388+
let models = models.clone();
389+
let conn = conn.clone();
390+
async move |_| {
391+
let result = conn.set_session_model(acp::SetSessionModelRequest {
392+
session_id,
393+
model_id: default_model,
394+
meta: None,
395+
})
396+
.await.log_err();
397+
398+
if result.is_none() {
399+
models.borrow_mut().current_model_id = initial_model_id;
400+
}
401+
}
402+
}).detach();
403+
404+
models_ref.current_model_id = default_model;
405+
} else {
406+
let available_models = models_ref
407+
.available_models
408+
.iter()
409+
.map(|model| format!("- `{}`: {}", model.model_id, model.name))
410+
.collect::<Vec<_>>()
411+
.join("\n");
412+
413+
log::warn!(
414+
"`{default_model}` is not a valid {name} model. Available options:\n{available_models}",
415+
);
416+
}
417+
} else {
418+
log::warn!(
419+
"`{name}` does not support model selection, but `default_model` was set in settings.",
420+
);
421+
}
422+
}
423+
370424
let session_id = response.session_id;
371425
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
372426
let thread = cx.new(|cx| {

crates/agent_servers/src/agent_servers.rs

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,18 @@ pub trait AgentServer: Send {
6868
) {
6969
}
7070

71+
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
72+
None
73+
}
74+
75+
fn set_default_model(
76+
&self,
77+
_model_id: Option<agent_client_protocol::ModelId>,
78+
_fs: Arc<dyn Fs>,
79+
_cx: &mut App,
80+
) {
81+
}
82+
7183
fn connect(
7284
&self,
7385
root_dir: Option<&Path>,

crates/agent_servers/src/claude.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,27 @@ impl AgentServer for ClaudeCode {
5555
});
5656
}
5757

58+
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
59+
let settings = cx.read_global(|settings: &SettingsStore, _| {
60+
settings.get::<AllAgentServersSettings>(None).claude.clone()
61+
});
62+
63+
settings
64+
.as_ref()
65+
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
66+
}
67+
68+
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
69+
update_settings_file(fs, cx, |settings, _| {
70+
settings
71+
.agent_servers
72+
.get_or_insert_default()
73+
.claude
74+
.get_or_insert_default()
75+
.default_model = model_id.map(|m| m.to_string())
76+
});
77+
}
78+
5879
fn connect(
5980
&self,
6081
root_dir: Option<&Path>,
@@ -68,6 +89,7 @@ impl AgentServer for ClaudeCode {
6889
let store = delegate.store.downgrade();
6990
let extra_env = load_proxy_env(cx);
7091
let default_mode = self.default_mode(cx);
92+
let default_model = self.default_model(cx);
7193

7294
cx.spawn(async move |cx| {
7395
let (command, root_dir, login) = store
@@ -90,6 +112,7 @@ impl AgentServer for ClaudeCode {
90112
command,
91113
root_dir.as_ref(),
92114
default_mode,
115+
default_model,
93116
is_remote,
94117
cx,
95118
)

crates/agent_servers/src/codex.rs

Lines changed: 23 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -56,6 +56,27 @@ impl AgentServer for Codex {
5656
});
5757
}
5858

59+
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
60+
let settings = cx.read_global(|settings: &SettingsStore, _| {
61+
settings.get::<AllAgentServersSettings>(None).codex.clone()
62+
});
63+
64+
settings
65+
.as_ref()
66+
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
67+
}
68+
69+
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
70+
update_settings_file(fs, cx, |settings, _| {
71+
settings
72+
.agent_servers
73+
.get_or_insert_default()
74+
.codex
75+
.get_or_insert_default()
76+
.default_model = model_id.map(|m| m.to_string())
77+
});
78+
}
79+
5980
fn connect(
6081
&self,
6182
root_dir: Option<&Path>,
@@ -69,6 +90,7 @@ impl AgentServer for Codex {
6990
let store = delegate.store.downgrade();
7091
let extra_env = load_proxy_env(cx);
7192
let default_mode = self.default_mode(cx);
93+
let default_model = self.default_model(cx);
7294

7395
cx.spawn(async move |cx| {
7496
let (command, root_dir, login) = store
@@ -92,6 +114,7 @@ impl AgentServer for Codex {
92114
command,
93115
root_dir.as_ref(),
94116
default_mode,
117+
default_model,
95118
is_remote,
96119
cx,
97120
)

crates/agent_servers/src/custom.rs

Lines changed: 30 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -61,6 +61,34 @@ impl crate::AgentServer for CustomAgentServer {
6161
});
6262
}
6363

64+
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
65+
let settings = cx.read_global(|settings: &SettingsStore, _| {
66+
settings
67+
.get::<AllAgentServersSettings>(None)
68+
.custom
69+
.get(&self.name())
70+
.cloned()
71+
});
72+
73+
settings
74+
.as_ref()
75+
.and_then(|s| s.default_model.clone().map(|m| acp::ModelId(m.into())))
76+
}
77+
78+
fn set_default_model(&self, model_id: Option<acp::ModelId>, fs: Arc<dyn Fs>, cx: &mut App) {
79+
let name = self.name();
80+
update_settings_file(fs, cx, move |settings, _| {
81+
if let Some(settings) = settings
82+
.agent_servers
83+
.get_or_insert_default()
84+
.custom
85+
.get_mut(&name)
86+
{
87+
settings.default_model = model_id.map(|m| m.to_string())
88+
}
89+
});
90+
}
91+
6492
fn connect(
6593
&self,
6694
root_dir: Option<&Path>,
@@ -72,6 +100,7 @@ impl crate::AgentServer for CustomAgentServer {
72100
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
73101
let is_remote = delegate.project.read(cx).is_via_remote_server();
74102
let default_mode = self.default_mode(cx);
103+
let default_model = self.default_model(cx);
75104
let store = delegate.store.downgrade();
76105
let extra_env = load_proxy_env(cx);
77106

@@ -98,6 +127,7 @@ impl crate::AgentServer for CustomAgentServer {
98127
command,
99128
root_dir.as_ref(),
100129
default_mode,
130+
default_model,
101131
is_remote,
102132
cx,
103133
)

crates/agent_servers/src/e2e_tests.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -476,6 +476,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
476476
env: None,
477477
ignore_system_version: None,
478478
default_mode: None,
479+
default_model: None,
479480
}),
480481
gemini: Some(crate::gemini::tests::local_command().into()),
481482
codex: Some(BuiltinAgentServerSettings {
@@ -484,6 +485,7 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
484485
env: None,
485486
ignore_system_version: None,
486487
default_mode: None,
488+
default_model: None,
487489
}),
488490
custom: collections::HashMap::default(),
489491
},

crates/agent_servers/src/gemini.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,7 @@ impl AgentServer for Gemini {
3737
let store = delegate.store.downgrade();
3838
let mut extra_env = load_proxy_env(cx);
3939
let default_mode = self.default_mode(cx);
40+
let default_model = self.default_model(cx);
4041

4142
cx.spawn(async move |cx| {
4243
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
@@ -69,6 +70,7 @@ impl AgentServer for Gemini {
6970
command,
7071
root_dir.as_ref(),
7172
default_mode,
73+
default_model,
7274
is_remote,
7375
cx,
7476
)

crates/agent_ui/src/acp/mode_selector.rs

Lines changed: 3 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -11,7 +11,7 @@ use ui::{
1111
PopoverMenu, PopoverMenuHandle, Tooltip, prelude::*,
1212
};
1313

14-
use crate::{CycleModeSelector, ToggleProfileSelector};
14+
use crate::{CycleModeSelector, ToggleProfileSelector, ui::HoldForDefault};
1515

1616
pub struct ModeSelector {
1717
connection: Rc<dyn AgentSessionModes>,
@@ -108,36 +108,11 @@ impl ModeSelector {
108108
entry.documentation_aside(side, DocumentationEdge::Bottom, {
109109
let description = description.clone();
110110

111-
move |cx| {
111+
move |_| {
112112
v_flex()
113113
.gap_1()
114114
.child(Label::new(description.clone()))
115-
.child(
116-
h_flex()
117-
.pt_1()
118-
.border_t_1()
119-
.border_color(cx.theme().colors().border_variant)
120-
.gap_0p5()
121-
.text_sm()
122-
.text_color(Color::Muted.color(cx))
123-
.child("Hold")
124-
.child(h_flex().flex_shrink_0().children(
125-
ui::render_modifiers(
126-
&gpui::Modifiers::secondary_key(),
127-
PlatformStyle::platform(),
128-
None,
129-
Some(ui::TextSize::Default.rems(cx).into()),
130-
true,
131-
),
132-
))
133-
.child(div().map(|this| {
134-
if is_default {
135-
this.child("to also unset as default")
136-
} else {
137-
this.child("to also set as default")
138-
}
139-
})),
140-
)
115+
.child(HoldForDefault::new(is_default))
141116
.into_any_element()
142117
}
143118
})

0 commit comments

Comments
 (0)