Detailed changes
@@ -10,6 +10,7 @@ dependencies = [
"agent-client-protocol",
"anyhow",
"buffer_diff",
+ "collections",
"editor",
"env_logger 0.11.8",
"futures 0.3.31",
@@ -17,7 +18,6 @@ dependencies = [
"indoc",
"itertools 0.14.0",
"language",
- "language_model",
"markdown",
"parking_lot",
"project",
@@ -31,6 +31,7 @@ dependencies = [
"ui",
"url",
"util",
+ "watch",
"workspace-hack",
]
@@ -20,12 +20,12 @@ action_log.workspace = true
agent-client-protocol.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
+collections.workspace = true
editor.workspace = true
futures.workspace = true
gpui.workspace = true
itertools.workspace = true
language.workspace = true
-language_model.workspace = true
markdown.workspace = true
project.workspace = true
serde.workspace = true
@@ -36,6 +36,7 @@ terminal.workspace = true
ui.workspace = true
url.workspace = true
util.workspace = true
+watch.workspace = true
workspace-hack.workspace = true
[dev-dependencies]
@@ -694,6 +694,10 @@ impl AcpThread {
}
}
+ pub fn connection(&self) -> &Rc<dyn AgentConnection> {
+ &self.connection
+ }
+
pub fn action_log(&self) -> &Entity<ActionLog> {
&self.action_log
}
@@ -1,18 +1,54 @@
-use std::{error::Error, fmt, path::Path, rc::Rc, sync::Arc};
+use std::{error::Error, fmt, path::Path, rc::Rc};
use agent_client_protocol::{self as acp};
use anyhow::Result;
-use gpui::{AsyncApp, Entity, Task};
-use language_model::LanguageModel;
+use collections::IndexMap;
+use gpui::{AsyncApp, Entity, SharedString, Task};
use project::Project;
-use ui::App;
+use ui::{App, IconName};
use crate::AcpThread;
+pub trait AgentConnection {
+ fn new_thread(
+ self: Rc<Self>,
+ project: Entity<Project>,
+ cwd: &Path,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<Entity<AcpThread>>>;
+
+ fn auth_methods(&self) -> &[acp::AuthMethod];
+
+ fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
+
+ fn prompt(&self, params: acp::PromptRequest, cx: &mut App)
+ -> Task<Result<acp::PromptResponse>>;
+
+ fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
+
+ /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
+ ///
+ /// If the agent does not support model selection, returns [None].
+ /// This allows sharing the selector in UI components.
+ fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
+ None
+ }
+}
+
+#[derive(Debug)]
+pub struct AuthRequired;
+
+impl Error for AuthRequired {}
+impl fmt::Display for AuthRequired {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "AuthRequired")
+ }
+}
+
/// Trait for agents that support listing, selecting, and querying language models.
///
/// This is an optional capability; agents indicate support via [AgentConnection::model_selector].
-pub trait ModelSelector: 'static {
+pub trait AgentModelSelector: 'static {
/// Lists all available language models for this agent.
///
/// # Parameters
@@ -20,7 +56,7 @@ pub trait ModelSelector: 'static {
///
/// # Returns
/// A task resolving to the list of models or an error (e.g., if no models are configured).
- fn list_models(&self, cx: &mut AsyncApp) -> Task<Result<Vec<Arc<dyn LanguageModel>>>>;
+ fn list_models(&self, cx: &mut App) -> Task<Result<AgentModelList>>;
/// Selects a model for a specific session (thread).
///
@@ -37,8 +73,8 @@ pub trait ModelSelector: 'static {
fn select_model(
&self,
session_id: acp::SessionId,
- model: Arc<dyn LanguageModel>,
- cx: &mut AsyncApp,
+ model_id: AgentModelId,
+ cx: &mut App,
) -> Task<Result<()>>;
/// Retrieves the currently selected model for a specific session (thread).
@@ -52,42 +88,51 @@ pub trait ModelSelector: 'static {
fn selected_model(
&self,
session_id: &acp::SessionId,
- cx: &mut AsyncApp,
- ) -> Task<Result<Arc<dyn LanguageModel>>>;
-}
+ cx: &mut App,
+ ) -> Task<Result<AgentModelInfo>>;
-pub trait AgentConnection {
- fn new_thread(
- self: Rc<Self>,
- project: Entity<Project>,
- cwd: &Path,
- cx: &mut AsyncApp,
- ) -> Task<Result<Entity<AcpThread>>>;
-
- fn auth_methods(&self) -> &[acp::AuthMethod];
+ /// Whenever the model list is updated the receiver will be notified.
+ fn watch(&self, cx: &mut App) -> watch::Receiver<()>;
+}
- fn authenticate(&self, method: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>>;
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct AgentModelId(pub SharedString);
- fn prompt(&self, params: acp::PromptRequest, cx: &mut App)
- -> Task<Result<acp::PromptResponse>>;
+impl std::ops::Deref for AgentModelId {
+ type Target = SharedString;
- fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
+ fn deref(&self) -> &Self::Target {
+ &self.0
+ }
+}
- /// Returns this agent as an [Rc<dyn ModelSelector>] if the model selection capability is supported.
- ///
- /// If the agent does not support model selection, returns [None].
- /// This allows sharing the selector in UI components.
- fn model_selector(&self) -> Option<Rc<dyn ModelSelector>> {
- None // Default impl for agents that don't support it
+impl fmt::Display for AgentModelId {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ self.0.fmt(f)
}
}
-#[derive(Debug)]
-pub struct AuthRequired;
+#[derive(Debug, Clone, PartialEq, Eq)]
+pub struct AgentModelInfo {
+ pub id: AgentModelId,
+ pub name: SharedString,
+ pub icon: Option<IconName>,
+}
-impl Error for AuthRequired {}
-impl fmt::Display for AuthRequired {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(f, "AuthRequired")
+#[derive(Debug, Clone, PartialEq, Eq, Hash)]
+pub struct AgentModelGroupName(pub SharedString);
+
+#[derive(Debug, Clone)]
+pub enum AgentModelList {
+ Flat(Vec<AgentModelInfo>),
+ Grouped(IndexMap<AgentModelGroupName, Vec<AgentModelInfo>>),
+}
+
+impl AgentModelList {
+ pub fn is_empty(&self) -> bool {
+ match self {
+ AgentModelList::Flat(models) => models.is_empty(),
+ AgentModelList::Grouped(groups) => groups.is_empty(),
+ }
}
}
@@ -4,18 +4,22 @@ use crate::{
FetchTool, FindPathTool, GrepTool, ListDirectoryTool, MessageContent, MovePathTool, NowTool,
OpenTool, ReadFileTool, TerminalTool, ThinkingTool, ToolCallAuthorization, WebSearchTool,
};
-use acp_thread::ModelSelector;
+use acp_thread::AgentModelSelector;
use agent_client_protocol as acp;
+use agent_settings::AgentSettings;
use anyhow::{Context as _, Result, anyhow};
+use collections::{HashSet, IndexMap};
+use fs::Fs;
use futures::{StreamExt, future};
use gpui::{
App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
};
-use language_model::{LanguageModel, LanguageModelRegistry};
+use language_model::{LanguageModel, LanguageModelProvider, LanguageModelRegistry};
use project::{Project, ProjectItem, ProjectPath, Worktree};
use prompt_store::{
ProjectContext, PromptId, PromptStore, RulesFileContext, UserRulesContext, WorktreeContext,
};
+use settings::update_settings_file;
use std::cell::RefCell;
use std::collections::HashMap;
use std::path::Path;
@@ -48,6 +52,104 @@ struct Session {
_subscription: Subscription,
}
+pub struct LanguageModels {
+ /// Access language model by ID
+ models: HashMap<acp_thread::AgentModelId, Arc<dyn LanguageModel>>,
+ /// Cached list for returning language model information
+ model_list: acp_thread::AgentModelList,
+ refresh_models_rx: watch::Receiver<()>,
+ refresh_models_tx: watch::Sender<()>,
+}
+
+impl LanguageModels {
+ fn new(cx: &App) -> Self {
+ let (refresh_models_tx, refresh_models_rx) = watch::channel(());
+ let mut this = Self {
+ models: HashMap::default(),
+ model_list: acp_thread::AgentModelList::Grouped(IndexMap::default()),
+ refresh_models_rx,
+ refresh_models_tx,
+ };
+ this.refresh_list(cx);
+ this
+ }
+
+ fn refresh_list(&mut self, cx: &App) {
+ let providers = LanguageModelRegistry::global(cx)
+ .read(cx)
+ .providers()
+ .into_iter()
+ .filter(|provider| provider.is_authenticated(cx))
+ .collect::<Vec<_>>();
+
+ let mut language_model_list = IndexMap::default();
+ let mut recommended_models = HashSet::default();
+
+ let mut recommended = Vec::new();
+ for provider in &providers {
+ for model in provider.recommended_models(cx) {
+ recommended_models.insert(model.id());
+ recommended.push(Self::map_language_model_to_info(&model, &provider));
+ }
+ }
+ if !recommended.is_empty() {
+ language_model_list.insert(
+ acp_thread::AgentModelGroupName("Recommended".into()),
+ recommended,
+ );
+ }
+
+ let mut models = HashMap::default();
+ for provider in providers {
+ let mut provider_models = Vec::new();
+ for model in provider.provided_models(cx) {
+ let model_info = Self::map_language_model_to_info(&model, &provider);
+ let model_id = model_info.id.clone();
+ if !recommended_models.contains(&model.id()) {
+ provider_models.push(model_info);
+ }
+ models.insert(model_id, model);
+ }
+ if !provider_models.is_empty() {
+ language_model_list.insert(
+ acp_thread::AgentModelGroupName(provider.name().0.clone()),
+ provider_models,
+ );
+ }
+ }
+
+ self.models = models;
+ self.model_list = acp_thread::AgentModelList::Grouped(language_model_list);
+ self.refresh_models_tx.send(()).ok();
+ }
+
+ fn watch(&self) -> watch::Receiver<()> {
+ self.refresh_models_rx.clone()
+ }
+
+ pub fn model_from_id(
+ &self,
+ model_id: &acp_thread::AgentModelId,
+ ) -> Option<Arc<dyn LanguageModel>> {
+ self.models.get(model_id).cloned()
+ }
+
+ fn map_language_model_to_info(
+ model: &Arc<dyn LanguageModel>,
+ provider: &Arc<dyn LanguageModelProvider>,
+ ) -> acp_thread::AgentModelInfo {
+ acp_thread::AgentModelInfo {
+ id: Self::model_id(model),
+ name: model.name().0,
+ icon: Some(provider.icon()),
+ }
+ }
+
+ fn model_id(model: &Arc<dyn LanguageModel>) -> acp_thread::AgentModelId {
+ acp_thread::AgentModelId(format!("{}/{}", model.provider_id().0, model.id().0).into())
+ }
+}
+
pub struct NativeAgent {
/// Session ID -> Session mapping
sessions: HashMap<acp::SessionId, Session>,
@@ -58,8 +160,11 @@ pub struct NativeAgent {
context_server_registry: Entity<ContextServerRegistry>,
/// Shared templates for all threads
templates: Arc<Templates>,
+ /// Cached model information
+ models: LanguageModels,
project: Entity<Project>,
prompt_store: Option<Entity<PromptStore>>,
+ fs: Arc<dyn Fs>,
_subscriptions: Vec<Subscription>,
}
@@ -68,6 +173,7 @@ impl NativeAgent {
project: Entity<Project>,
templates: Arc<Templates>,
prompt_store: Option<Entity<PromptStore>>,
+ fs: Arc<dyn Fs>,
cx: &mut AsyncApp,
) -> Result<Entity<NativeAgent>> {
log::info!("Creating new NativeAgent");
@@ -77,7 +183,13 @@ impl NativeAgent {
.await;
cx.new(|cx| {
- let mut subscriptions = vec![cx.subscribe(&project, Self::handle_project_event)];
+ let mut subscriptions = vec![
+ cx.subscribe(&project, Self::handle_project_event),
+ cx.subscribe(
+ &LanguageModelRegistry::global(cx),
+ Self::handle_models_updated_event,
+ ),
+ ];
if let Some(prompt_store) = prompt_store.as_ref() {
subscriptions.push(cx.subscribe(prompt_store, Self::handle_prompts_updated_event))
}
@@ -95,13 +207,19 @@ impl NativeAgent {
ContextServerRegistry::new(project.read(cx).context_server_store(), cx)
}),
templates,
+ models: LanguageModels::new(cx),
project,
prompt_store,
+ fs,
_subscriptions: subscriptions,
}
})
}
+ pub fn models(&self) -> &LanguageModels {
+ &self.models
+ }
+
async fn maintain_project_context(
this: WeakEntity<Self>,
mut needs_refresh: watch::Receiver<()>,
@@ -297,75 +415,104 @@ impl NativeAgent {
) {
self.project_context_needs_refresh.send(()).ok();
}
+
+ fn handle_models_updated_event(
+ &mut self,
+ _registry: Entity<LanguageModelRegistry>,
+ _event: &language_model::Event,
+ cx: &mut Context<Self>,
+ ) {
+ self.models.refresh_list(cx);
+ for session in self.sessions.values_mut() {
+ session.thread.update(cx, |thread, _| {
+ let model_id = LanguageModels::model_id(&thread.selected_model);
+ if let Some(model) = self.models.model_from_id(&model_id) {
+ thread.selected_model = model.clone();
+ }
+ });
+ }
+ }
}
/// Wrapper struct that implements the AgentConnection trait
#[derive(Clone)]
pub struct NativeAgentConnection(pub Entity<NativeAgent>);
-impl ModelSelector for NativeAgentConnection {
- fn list_models(&self, cx: &mut AsyncApp) -> Task<Result<Vec<Arc<dyn LanguageModel>>>> {
+impl AgentModelSelector for NativeAgentConnection {
+ fn list_models(&self, cx: &mut App) -> Task<Result<acp_thread::AgentModelList>> {
log::debug!("NativeAgentConnection::list_models called");
- cx.spawn(async move |cx| {
- cx.update(|cx| {
- let registry = LanguageModelRegistry::read_global(cx);
- let models = registry.available_models(cx).collect::<Vec<_>>();
- log::info!("Found {} available models", models.len());
- if models.is_empty() {
- Err(anyhow::anyhow!("No models available"))
- } else {
- Ok(models)
- }
- })?
+ let list = self.0.read(cx).models.model_list.clone();
+ Task::ready(if list.is_empty() {
+ Err(anyhow::anyhow!("No models available"))
+ } else {
+ Ok(list)
})
}
fn select_model(
&self,
session_id: acp::SessionId,
- model: Arc<dyn LanguageModel>,
- cx: &mut AsyncApp,
+ model_id: acp_thread::AgentModelId,
+ cx: &mut App,
) -> Task<Result<()>> {
- log::info!(
- "Setting model for session {}: {:?}",
- session_id,
- model.name()
+ log::info!("Setting model for session {}: {}", session_id, model_id);
+ let Some(thread) = self
+ .0
+ .read(cx)
+ .sessions
+ .get(&session_id)
+ .map(|session| session.thread.clone())
+ else {
+ return Task::ready(Err(anyhow!("Session not found")));
+ };
+
+ let Some(model) = self.0.read(cx).models.model_from_id(&model_id) else {
+ return Task::ready(Err(anyhow!("Invalid model ID {}", model_id)));
+ };
+
+ thread.update(cx, |thread, _cx| {
+ thread.selected_model = model.clone();
+ });
+
+ update_settings_file::<AgentSettings>(
+ self.0.read(cx).fs.clone(),
+ cx,
+ move |settings, _cx| {
+ settings.set_model(model);
+ },
);
- let agent = self.0.clone();
- cx.spawn(async move |cx| {
- agent.update(cx, |agent, cx| {
- if let Some(session) = agent.sessions.get(&session_id) {
- session.thread.update(cx, |thread, _cx| {
- thread.selected_model = model;
- });
- Ok(())
- } else {
- Err(anyhow!("Session not found"))
- }
- })?
- })
+ Task::ready(Ok(()))
}
fn selected_model(
&self,
session_id: &acp::SessionId,
- cx: &mut AsyncApp,
- ) -> Task<Result<Arc<dyn LanguageModel>>> {
- let agent = self.0.clone();
+ cx: &mut App,
+ ) -> Task<Result<acp_thread::AgentModelInfo>> {
let session_id = session_id.clone();
- cx.spawn(async move |cx| {
- let thread = agent
- .read_with(cx, |agent, _| {
- agent
- .sessions
- .get(&session_id)
- .map(|session| session.thread.clone())
- })?
- .ok_or_else(|| anyhow::anyhow!("Session not found"))?;
- let selected = thread.read_with(cx, |thread, _| thread.selected_model.clone())?;
- Ok(selected)
- })
+
+ let Some(thread) = self
+ .0
+ .read(cx)
+ .sessions
+ .get(&session_id)
+ .map(|session| session.thread.clone())
+ else {
+ return Task::ready(Err(anyhow!("Session not found")));
+ };
+ let model = thread.read(cx).selected_model.clone();
+ let Some(provider) = LanguageModelRegistry::read_global(cx).provider(&model.provider_id())
+ else {
+ return Task::ready(Err(anyhow!("Provider not found")));
+ };
+ Task::ready(Ok(LanguageModels::map_language_model_to_info(
+ &model, &provider,
+ )))
+ }
+
+ fn watch(&self, cx: &mut App) -> watch::Receiver<()> {
+ self.0.read(cx).models.watch()
}
}
@@ -413,13 +560,10 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
let default_model = registry
.default_model()
- .map(|configured| {
- log::info!(
- "Using configured default model: {:?} from provider: {:?}",
- configured.model.name(),
- configured.provider.name()
- );
- configured.model
+ .and_then(|default_model| {
+ agent
+ .models
+ .model_from_id(&LanguageModels::model_id(&default_model.model))
})
.ok_or_else(|| {
log::warn!("No default model configured in settings");
@@ -487,8 +631,8 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
Task::ready(Ok(()))
}
- fn model_selector(&self) -> Option<Rc<dyn ModelSelector>> {
- Some(Rc::new(self.clone()) as Rc<dyn ModelSelector>)
+ fn model_selector(&self) -> Option<Rc<dyn AgentModelSelector>> {
+ Some(Rc::new(self.clone()) as Rc<dyn AgentModelSelector>)
}
fn prompt(
@@ -629,6 +773,7 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
#[cfg(test)]
mod tests {
use super::*;
+ use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelId, AgentModelInfo};
use fs::FakeFs;
use gpui::TestAppContext;
use serde_json::json;
@@ -646,9 +791,15 @@ mod tests {
)
.await;
let project = Project::test(fs.clone(), [], cx).await;
- let agent = NativeAgent::new(project.clone(), Templates::new(), None, &mut cx.to_async())
- .await
- .unwrap();
+ let agent = NativeAgent::new(
+ project.clone(),
+ Templates::new(),
+ None,
+ fs.clone(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
agent.read_with(cx, |agent, _| {
assert_eq!(agent.project_context.borrow().worktrees, vec![])
});
@@ -689,13 +840,131 @@ mod tests {
});
}
+ #[gpui::test]
+ async fn test_listing_models(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ fs.insert_tree("/", json!({ "a": {} })).await;
+ let project = Project::test(fs.clone(), [], cx).await;
+ let connection = NativeAgentConnection(
+ NativeAgent::new(
+ project.clone(),
+ Templates::new(),
+ None,
+ fs.clone(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap(),
+ );
+
+ let models = cx.update(|cx| connection.list_models(cx)).await.unwrap();
+
+ let acp_thread::AgentModelList::Grouped(models) = models else {
+ panic!("Unexpected model group");
+ };
+ assert_eq!(
+ models,
+ IndexMap::from_iter([(
+ AgentModelGroupName("Fake".into()),
+ vec![AgentModelInfo {
+ id: AgentModelId("fake/fake".into()),
+ name: "Fake".into(),
+ icon: Some(ui::IconName::ZedAssistant),
+ }]
+ )])
+ );
+ }
+
+ #[gpui::test]
+ async fn test_model_selection_persists_to_settings(cx: &mut TestAppContext) {
+ init_test(cx);
+ let fs = FakeFs::new(cx.executor());
+ fs.create_dir(paths::settings_file().parent().unwrap())
+ .await
+ .unwrap();
+ fs.insert_file(
+ paths::settings_file(),
+ json!({
+ "agent": {
+ "default_model": {
+ "provider": "foo",
+ "model": "bar"
+ }
+ }
+ })
+ .to_string()
+ .into_bytes(),
+ )
+ .await;
+ let project = Project::test(fs.clone(), [], cx).await;
+
+ // Create the agent and connection
+ let agent = NativeAgent::new(
+ project.clone(),
+ Templates::new(),
+ None,
+ fs.clone(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
+ let connection = NativeAgentConnection(agent.clone());
+
+ // Create a thread/session
+ let acp_thread = cx
+ .update(|cx| {
+ Rc::new(connection.clone()).new_thread(
+ project.clone(),
+ Path::new("/a"),
+ &mut cx.to_async(),
+ )
+ })
+ .await
+ .unwrap();
+
+ let session_id = cx.update(|cx| acp_thread.read(cx).session_id().clone());
+
+ // Select a model
+ let model_id = AgentModelId("fake/fake".into());
+ cx.update(|cx| connection.select_model(session_id.clone(), model_id.clone(), cx))
+ .await
+ .unwrap();
+
+ // Verify the thread has the selected model
+ agent.read_with(cx, |agent, _| {
+ let session = agent.sessions.get(&session_id).unwrap();
+ session.thread.read_with(cx, |thread, _| {
+ assert_eq!(thread.selected_model.id().0, "fake");
+ });
+ });
+
+ cx.run_until_parked();
+
+ // Verify settings file was updated
+ let settings_content = fs.load(paths::settings_file()).await.unwrap();
+ let settings_json: serde_json::Value = serde_json::from_str(&settings_content).unwrap();
+
+ // Check that the agent settings contain the selected model
+ assert_eq!(
+ settings_json["agent"]["default_model"]["model"],
+ json!("fake")
+ );
+ assert_eq!(
+ settings_json["agent"]["default_model"]["provider"],
+ json!("fake")
+ );
+ }
+
fn init_test(cx: &mut TestAppContext) {
env_logger::try_init().ok();
cx.update(|cx| {
let settings_store = SettingsStore::test(cx);
cx.set_global(settings_store);
Project::init_settings(cx);
+ agent_settings::init(cx);
language::init(cx);
+ LanguageModelRegistry::test(cx);
});
}
}
@@ -1,8 +1,8 @@
-use std::path::Path;
-use std::rc::Rc;
+use std::{path::Path, rc::Rc, sync::Arc};
use agent_servers::AgentServer;
use anyhow::Result;
+use fs::Fs;
use gpui::{App, Entity, Task};
use project::Project;
use prompt_store::PromptStore;
@@ -10,7 +10,15 @@ use prompt_store::PromptStore;
use crate::{NativeAgent, NativeAgentConnection, templates::Templates};
#[derive(Clone)]
-pub struct NativeAgentServer;
+pub struct NativeAgentServer {
+ fs: Arc<dyn Fs>,
+}
+
+impl NativeAgentServer {
+ pub fn new(fs: Arc<dyn Fs>) -> Self {
+ Self { fs }
+ }
+}
impl AgentServer for NativeAgentServer {
fn name(&self) -> &'static str {
@@ -41,6 +49,7 @@ impl AgentServer for NativeAgentServer {
_root_dir
);
let project = project.clone();
+ let fs = self.fs.clone();
let prompt_store = PromptStore::global(cx);
cx.spawn(async move |cx| {
log::debug!("Creating templates for native agent");
@@ -48,7 +57,7 @@ impl AgentServer for NativeAgentServer {
let prompt_store = prompt_store.await?;
log::debug!("Creating native agent entity");
- let agent = NativeAgent::new(project, templates, Some(prompt_store), cx).await?;
+ let agent = NativeAgent::new(project, templates, Some(prompt_store), fs, cx).await?;
// Create the connection wrapper
let connection = NativeAgentConnection(agent);
@@ -1,6 +1,6 @@
use super::*;
use crate::MessageContent;
-use acp_thread::AgentConnection;
+use acp_thread::{AgentConnection, AgentModelGroupName, AgentModelList};
use action_log::ActionLog;
use agent_client_protocol::{self as acp};
use agent_settings::AgentProfileId;
@@ -686,13 +686,19 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
// Create a project for new_thread
let fake_fs = cx.update(|cx| fs::FakeFs::new(cx.background_executor().clone()));
fake_fs.insert_tree(path!("/test"), json!({})).await;
- let project = Project::test(fake_fs, [Path::new("/test")], cx).await;
+ let project = Project::test(fake_fs.clone(), [Path::new("/test")], cx).await;
let cwd = Path::new("/test");
// Create agent and connection
- let agent = NativeAgent::new(project.clone(), templates.clone(), None, &mut cx.to_async())
- .await
- .unwrap();
+ let agent = NativeAgent::new(
+ project.clone(),
+ templates.clone(),
+ None,
+ fake_fs.clone(),
+ &mut cx.to_async(),
+ )
+ .await
+ .unwrap();
let connection = NativeAgentConnection(agent.clone());
// Test model_selector returns Some
@@ -705,22 +711,22 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
// Test list_models
let listed_models = cx
- .update(|cx| {
- let mut async_cx = cx.to_async();
- selector.list_models(&mut async_cx)
- })
+ .update(|cx| selector.list_models(cx))
.await
.expect("list_models should succeed");
+ let AgentModelList::Grouped(listed_models) = listed_models else {
+ panic!("Unexpected model list type");
+ };
assert!(!listed_models.is_empty(), "should have at least one model");
- assert_eq!(listed_models[0].id().0, "fake");
+ assert_eq!(
+ listed_models[&AgentModelGroupName("Fake".into())][0].id.0,
+ "fake/fake"
+ );
// Create a thread using new_thread
let connection_rc = Rc::new(connection.clone());
let acp_thread = cx
- .update(|cx| {
- let mut async_cx = cx.to_async();
- connection_rc.new_thread(project, cwd, &mut async_cx)
- })
+ .update(|cx| connection_rc.new_thread(project, cwd, &mut cx.to_async()))
.await
.expect("new_thread should succeed");
@@ -729,12 +735,12 @@ async fn test_agent_connection(cx: &mut TestAppContext) {
// Test selected_model returns the default
let model = cx
- .update(|cx| {
- let mut async_cx = cx.to_async();
- selector.selected_model(&session_id, &mut async_cx)
- })
+ .update(|cx| selector.selected_model(&session_id, cx))
.await
.expect("selected_model should succeed");
+ let model = cx
+ .update(|cx| agent.read(cx).models().model_from_id(&model.id))
+ .unwrap();
let model = model.as_fake();
assert_eq!(model.id().0, "fake", "should return default model");
@@ -1,6 +1,10 @@
mod completion_provider;
mod message_history;
+mod model_selector;
+mod model_selector_popover;
mod thread_view;
pub use message_history::MessageHistory;
+pub use model_selector::AcpModelSelector;
+pub use model_selector_popover::AcpModelSelectorPopover;
pub use thread_view::AcpThreadView;
@@ -0,0 +1,472 @@
+use std::{cmp::Reverse, rc::Rc, sync::Arc};
+
+use acp_thread::{AgentModelInfo, AgentModelList, AgentModelSelector};
+use agent_client_protocol as acp;
+use anyhow::Result;
+use collections::IndexMap;
+use futures::FutureExt;
+use fuzzy::{StringMatchCandidate, match_strings};
+use gpui::{Action, AsyncWindowContext, BackgroundExecutor, DismissEvent, Task, WeakEntity};
+use ordered_float::OrderedFloat;
+use picker::{Picker, PickerDelegate};
+use ui::{
+ AnyElement, App, Context, IntoElement, ListItem, ListItemSpacing, SharedString, Window,
+ prelude::*, rems,
+};
+use util::ResultExt;
+
+pub type AcpModelSelector = Picker<AcpModelPickerDelegate>;
+
+pub fn acp_model_selector(
+ session_id: acp::SessionId,
+ selector: Rc<dyn AgentModelSelector>,
+ window: &mut Window,
+ cx: &mut Context<AcpModelSelector>,
+) -> AcpModelSelector {
+ let delegate = AcpModelPickerDelegate::new(session_id, selector, window, cx);
+ Picker::list(delegate, window, cx)
+ .show_scrollbar(true)
+ .width(rems(20.))
+ .max_height(Some(rems(20.).into()))
+}
+
+enum AcpModelPickerEntry {
+ Separator(SharedString),
+ Model(AgentModelInfo),
+}
+
+pub struct AcpModelPickerDelegate {
+ session_id: acp::SessionId,
+ selector: Rc<dyn AgentModelSelector>,
+ filtered_entries: Vec<AcpModelPickerEntry>,
+ models: Option<AgentModelList>,
+ selected_index: usize,
+ selected_model: Option<AgentModelInfo>,
+ _refresh_models_task: Task<()>,
+}
+
+impl AcpModelPickerDelegate {
+ fn new(
+ session_id: acp::SessionId,
+ selector: Rc<dyn AgentModelSelector>,
+ window: &mut Window,
+ cx: &mut Context<AcpModelSelector>,
+ ) -> Self {
+ let mut rx = selector.watch(cx);
+ let refresh_models_task = cx.spawn_in(window, {
+ let session_id = session_id.clone();
+ async move |this, cx| {
+ async fn refresh(
+ this: &WeakEntity<Picker<AcpModelPickerDelegate>>,
+ session_id: &acp::SessionId,
+ cx: &mut AsyncWindowContext,
+ ) -> Result<()> {
+ let (models_task, selected_model_task) = this.update(cx, |this, cx| {
+ (
+ this.delegate.selector.list_models(cx),
+ this.delegate.selector.selected_model(session_id, cx),
+ )
+ })?;
+
+ let (models, selected_model) = futures::join!(models_task, selected_model_task);
+
+ this.update_in(cx, |this, window, cx| {
+ this.delegate.models = models.ok();
+ this.delegate.selected_model = selected_model.ok();
+ this.delegate.update_matches(this.query(cx), window, cx)
+ })?
+ .await;
+
+ Ok(())
+ }
+
+ refresh(&this, &session_id, cx).await.log_err();
+ while let Ok(()) = rx.recv().await {
+ refresh(&this, &session_id, cx).await.log_err();
+ }
+ }
+ });
+
+ Self {
+ session_id,
+ selector,
+ filtered_entries: Vec::new(),
+ models: None,
+ selected_model: None,
+ selected_index: 0,
+ _refresh_models_task: refresh_models_task,
+ }
+ }
+
+ pub fn active_model(&self) -> Option<&AgentModelInfo> {
+ self.selected_model.as_ref()
+ }
+}
+
+impl PickerDelegate for AcpModelPickerDelegate {
+ type ListItem = AnyElement;
+
+ fn match_count(&self) -> usize {
+ self.filtered_entries.len()
+ }
+
+ fn selected_index(&self) -> usize {
+ self.selected_index
+ }
+
+ fn set_selected_index(&mut self, ix: usize, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+ self.selected_index = ix.min(self.filtered_entries.len().saturating_sub(1));
+ cx.notify();
+ }
+
+ fn can_select(
+ &mut self,
+ ix: usize,
+ _window: &mut Window,
+ _cx: &mut Context<Picker<Self>>,
+ ) -> bool {
+ match self.filtered_entries.get(ix) {
+ Some(AcpModelPickerEntry::Model(_)) => true,
+ Some(AcpModelPickerEntry::Separator(_)) | None => false,
+ }
+ }
+
+ fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
+ "Select a modelβ¦".into()
+ }
+
+ fn update_matches(
+ &mut self,
+ query: String,
+ window: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Task<()> {
+ cx.spawn_in(window, async move |this, cx| {
+ let filtered_models = match this
+ .read_with(cx, |this, cx| {
+ this.delegate.models.clone().map(move |models| {
+ fuzzy_search(models, query, cx.background_executor().clone())
+ })
+ })
+ .ok()
+ .flatten()
+ {
+ Some(task) => task.await,
+ None => AgentModelList::Flat(vec![]),
+ };
+
+ this.update_in(cx, |this, window, cx| {
+ this.delegate.filtered_entries =
+ info_list_to_picker_entries(filtered_models).collect();
+ // Finds the currently selected model in the list
+ let new_index = this
+ .delegate
+ .selected_model
+ .as_ref()
+ .and_then(|selected| {
+ this.delegate.filtered_entries.iter().position(|entry| {
+ if let AcpModelPickerEntry::Model(model_info) = entry {
+ model_info.id == selected.id
+ } else {
+ false
+ }
+ })
+ })
+ .unwrap_or(0);
+ this.set_selected_index(new_index, Some(picker::Direction::Down), true, window, cx);
+ cx.notify();
+ })
+ .ok();
+ })
+ }
+
+ fn confirm(&mut self, _secondary: bool, window: &mut Window, cx: &mut Context<Picker<Self>>) {
+ if let Some(AcpModelPickerEntry::Model(model_info)) =
+ self.filtered_entries.get(self.selected_index)
+ {
+ self.selector
+ .select_model(self.session_id.clone(), model_info.id.clone(), cx)
+ .detach_and_log_err(cx);
+ self.selected_model = Some(model_info.clone());
+ let current_index = self.selected_index;
+ self.set_selected_index(current_index, window, cx);
+
+ cx.emit(DismissEvent);
+ }
+ }
+
+ fn dismissed(&mut self, _: &mut Window, cx: &mut Context<Picker<Self>>) {
+ cx.emit(DismissEvent);
+ }
+
+ fn render_match(
+ &self,
+ ix: usize,
+ selected: bool,
+ _: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Option<Self::ListItem> {
+ match self.filtered_entries.get(ix)? {
+ AcpModelPickerEntry::Separator(title) => Some(
+ div()
+ .px_2()
+ .pb_1()
+ .when(ix > 1, |this| {
+ this.mt_1()
+ .pt_2()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ })
+ .child(
+ Label::new(title)
+ .size(LabelSize::XSmall)
+ .color(Color::Muted),
+ )
+ .into_any_element(),
+ ),
+ AcpModelPickerEntry::Model(model_info) => {
+ let is_selected = Some(model_info) == self.selected_model.as_ref();
+
+ let model_icon_color = if is_selected {
+ Color::Accent
+ } else {
+ Color::Muted
+ };
+
+ Some(
+ ListItem::new(ix)
+ .inset(true)
+ .spacing(ListItemSpacing::Sparse)
+ .toggle_state(selected)
+ .start_slot::<Icon>(model_info.icon.map(|icon| {
+ Icon::new(icon)
+ .color(model_icon_color)
+ .size(IconSize::Small)
+ }))
+ .child(
+ h_flex()
+ .w_full()
+ .pl_0p5()
+ .gap_1p5()
+ .w(px(240.))
+ .child(Label::new(model_info.name.clone()).truncate()),
+ )
+ .end_slot(div().pr_3().when(is_selected, |this| {
+ this.child(
+ Icon::new(IconName::Check)
+ .color(Color::Accent)
+ .size(IconSize::Small),
+ )
+ }))
+ .into_any_element(),
+ )
+ }
+ }
+ }
+
+ fn render_footer(
+ &self,
+ _: &mut Window,
+ cx: &mut Context<Picker<Self>>,
+ ) -> Option<gpui::AnyElement> {
+ Some(
+ h_flex()
+ .w_full()
+ .border_t_1()
+ .border_color(cx.theme().colors().border_variant)
+ .p_1()
+ .gap_4()
+ .justify_between()
+ .child(
+ Button::new("configure", "Configure")
+ .icon(IconName::Settings)
+ .icon_size(IconSize::Small)
+ .icon_color(Color::Muted)
+ .icon_position(IconPosition::Start)
+ .on_click(|_, window, cx| {
+ window.dispatch_action(
+ zed_actions::agent::OpenSettings.boxed_clone(),
+ cx,
+ );
+ }),
+ )
+ .into_any(),
+ )
+ }
+}
+
+fn info_list_to_picker_entries(
+ model_list: AgentModelList,
+) -> impl Iterator<Item = AcpModelPickerEntry> {
+ match model_list {
+ AgentModelList::Flat(list) => {
+ itertools::Either::Left(list.into_iter().map(AcpModelPickerEntry::Model))
+ }
+ AgentModelList::Grouped(index_map) => {
+ itertools::Either::Right(index_map.into_iter().flat_map(|(group_name, models)| {
+ std::iter::once(AcpModelPickerEntry::Separator(group_name.0))
+ .chain(models.into_iter().map(AcpModelPickerEntry::Model))
+ }))
+ }
+ }
+}
+
+async fn fuzzy_search(
+ model_list: AgentModelList,
+ query: String,
+ executor: BackgroundExecutor,
+) -> AgentModelList {
+ async fn fuzzy_search_list(
+ model_list: Vec<AgentModelInfo>,
+ query: &str,
+ executor: BackgroundExecutor,
+ ) -> Vec<AgentModelInfo> {
+ let candidates = model_list
+ .iter()
+ .enumerate()
+ .map(|(ix, model)| {
+ StringMatchCandidate::new(ix, &format!("{}/{}", model.id, model.name))
+ })
+ .collect::<Vec<_>>();
+ let mut matches = match_strings(
+ &candidates,
+ &query,
+ false,
+ true,
+ 100,
+ &Default::default(),
+ executor,
+ )
+ .await;
+
+ matches.sort_unstable_by_key(|mat| {
+ let candidate = &candidates[mat.candidate_id];
+ (Reverse(OrderedFloat(mat.score)), candidate.id)
+ });
+
+ matches
+ .into_iter()
+ .map(|mat| model_list[mat.candidate_id].clone())
+ .collect()
+ }
+
+ match model_list {
+ AgentModelList::Flat(model_list) => {
+ AgentModelList::Flat(fuzzy_search_list(model_list, &query, executor).await)
+ }
+ AgentModelList::Grouped(index_map) => {
+ let groups =
+ futures::future::join_all(index_map.into_iter().map(|(group_name, models)| {
+ fuzzy_search_list(models, &query, executor.clone())
+ .map(|results| (group_name, results))
+ }))
+ .await;
+ AgentModelList::Grouped(IndexMap::from_iter(
+ groups
+ .into_iter()
+ .filter(|(_, results)| !results.is_empty()),
+ ))
+ }
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use gpui::TestAppContext;
+
+ use super::*;
+
+ fn create_model_list(grouped_models: Vec<(&str, Vec<&str>)>) -> AgentModelList {
+ AgentModelList::Grouped(IndexMap::from_iter(grouped_models.into_iter().map(
+ |(group, models)| {
+ (
+ acp_thread::AgentModelGroupName(group.to_string().into()),
+ models
+ .into_iter()
+ .map(|model| acp_thread::AgentModelInfo {
+ id: acp_thread::AgentModelId(model.to_string().into()),
+ name: model.to_string().into(),
+ icon: None,
+ })
+ .collect::<Vec<_>>(),
+ )
+ },
+ )))
+ }
+
+ fn assert_models_eq(result: AgentModelList, expected: Vec<(&str, Vec<&str>)>) {
+ let AgentModelList::Grouped(groups) = result else {
+ panic!("Expected LanguageModelInfoList::Grouped, got {:?}", result);
+ };
+
+ assert_eq!(
+ groups.len(),
+ expected.len(),
+ "Number of groups doesn't match"
+ );
+
+ for (i, (expected_group, expected_models)) in expected.iter().enumerate() {
+ let (actual_group, actual_models) = groups.get_index(i).unwrap();
+ assert_eq!(
+ actual_group.0.as_ref(),
+ *expected_group,
+ "Group at position {} doesn't match expected group",
+ i
+ );
+ assert_eq!(
+ actual_models.len(),
+ expected_models.len(),
+ "Number of models in group {} doesn't match",
+ expected_group
+ );
+
+ for (j, expected_model_name) in expected_models.iter().enumerate() {
+ assert_eq!(
+ actual_models[j].name, *expected_model_name,
+ "Model at position {} in group {} doesn't match expected model",
+ j, expected_group
+ );
+ }
+ }
+ }
+
+ #[gpui::test]
+ async fn test_fuzzy_match(cx: &mut TestAppContext) {
+ let models = create_model_list(vec![
+ (
+ "zed",
+ vec![
+ "Claude 3.7 Sonnet",
+ "Claude 3.7 Sonnet Thinking",
+ "gpt-4.1",
+ "gpt-4.1-nano",
+ ],
+ ),
+ ("openai", vec!["gpt-3.5-turbo", "gpt-4.1", "gpt-4.1-nano"]),
+ ("ollama", vec!["mistral", "deepseek"]),
+ ]);
+
+ // Results should preserve models order whenever possible.
+ // In the case below, `zed/gpt-4.1` and `openai/gpt-4.1` have identical
+ // similarity scores, but `zed/gpt-4.1` was higher in the models list,
+ // so it should appear first in the results.
+ let results = fuzzy_search(models.clone(), "41".into(), cx.executor()).await;
+ assert_models_eq(
+ results,
+ vec![
+ ("zed", vec!["gpt-4.1", "gpt-4.1-nano"]),
+ ("openai", vec!["gpt-4.1", "gpt-4.1-nano"]),
+ ],
+ );
+
+ // Fuzzy search
+ let results = fuzzy_search(models.clone(), "4n".into(), cx.executor()).await;
+ assert_models_eq(
+ results,
+ vec![
+ ("zed", vec!["gpt-4.1-nano"]),
+ ("openai", vec!["gpt-4.1-nano"]),
+ ],
+ );
+ }
+}
@@ -0,0 +1,85 @@
+use std::rc::Rc;
+
+use acp_thread::AgentModelSelector;
+use agent_client_protocol as acp;
+use gpui::{Entity, FocusHandle};
+use picker::popover_menu::PickerPopoverMenu;
+use ui::{
+ ButtonLike, Context, IntoElement, PopoverMenuHandle, SharedString, Tooltip, Window, prelude::*,
+};
+use zed_actions::agent::ToggleModelSelector;
+
+use crate::acp::{AcpModelSelector, model_selector::acp_model_selector};
+
+pub struct AcpModelSelectorPopover {
+ selector: Entity<AcpModelSelector>,
+ menu_handle: PopoverMenuHandle<AcpModelSelector>,
+ focus_handle: FocusHandle,
+}
+
+impl AcpModelSelectorPopover {
+ pub(crate) fn new(
+ session_id: acp::SessionId,
+ selector: Rc<dyn AgentModelSelector>,
+ menu_handle: PopoverMenuHandle<AcpModelSelector>,
+ focus_handle: FocusHandle,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Self {
+ Self {
+ selector: cx.new(move |cx| acp_model_selector(session_id, selector, window, cx)),
+ menu_handle,
+ focus_handle,
+ }
+ }
+
+ pub fn toggle(&self, window: &mut Window, cx: &mut Context<Self>) {
+ self.menu_handle.toggle(window, cx);
+ }
+}
+
+impl Render for AcpModelSelectorPopover {
+ fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
+ let model = self.selector.read(cx).delegate.active_model();
+ let model_name = model
+ .as_ref()
+ .map(|model| model.name.clone())
+ .unwrap_or_else(|| SharedString::from("Select a Model"));
+
+ let model_icon = model.as_ref().and_then(|model| model.icon);
+
+ let focus_handle = self.focus_handle.clone();
+
+ PickerPopoverMenu::new(
+ self.selector.clone(),
+ ButtonLike::new("active-model")
+ .when_some(model_icon, |this, icon| {
+ this.child(Icon::new(icon).color(Color::Muted).size(IconSize::XSmall))
+ })
+ .child(
+ Label::new(model_name)
+ .color(Color::Muted)
+ .size(LabelSize::Small)
+ .ml_0p5(),
+ )
+ .child(
+ Icon::new(IconName::ChevronDown)
+ .color(Color::Muted)
+ .size(IconSize::XSmall),
+ ),
+ move |window, cx| {
+ Tooltip::for_action_in(
+ "Change Model",
+ &ToggleModelSelector,
+ &focus_handle,
+ window,
+ cx,
+ )
+ },
+ gpui::Corner::BottomRight,
+ cx,
+ )
+ .with_handle(self.menu_handle.clone())
+ .render(window, cx)
+ }
+}
@@ -38,12 +38,14 @@ use terminal_view::TerminalView;
use text::{Anchor, BufferSnapshot};
use theme::ThemeSettings;
use ui::{
- Disclosure, Divider, DividerColor, KeyBinding, Scrollbar, ScrollbarState, Tooltip, prelude::*,
+ Disclosure, Divider, DividerColor, KeyBinding, PopoverMenuHandle, Scrollbar, ScrollbarState,
+ Tooltip, prelude::*,
};
use util::{ResultExt, size::format_file_size, time::duration_alt_display};
use workspace::{CollaboratorId, Workspace};
-use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
+use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage, ToggleModelSelector};
+use crate::acp::AcpModelSelectorPopover;
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
use crate::acp::message_history::MessageHistory;
use crate::agent_diff::AgentDiff;
@@ -63,6 +65,7 @@ pub struct AcpThreadView {
diff_editors: HashMap<EntityId, Entity<Editor>>,
terminal_views: HashMap<EntityId, Entity<TerminalView>>,
message_editor: Entity<Editor>,
+ model_selector: Option<Entity<AcpModelSelectorPopover>>,
message_set_from_history: Option<BufferSnapshot>,
_message_editor_subscription: Subscription,
mention_set: Arc<Mutex<MentionSet>>,
@@ -187,6 +190,7 @@ impl AcpThreadView {
project: project.clone(),
thread_state: Self::initial_state(agent, workspace, project, window, cx),
message_editor,
+ model_selector: None,
message_set_from_history: None,
_message_editor_subscription: message_editor_subscription,
mention_set,
@@ -270,7 +274,7 @@ impl AcpThreadView {
Err(e)
}
}
- Ok(session_id) => Ok(session_id),
+ Ok(thread) => Ok(thread),
};
this.update_in(cx, |this, window, cx| {
@@ -288,6 +292,24 @@ impl AcpThreadView {
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
+ this.model_selector =
+ thread
+ .read(cx)
+ .connection()
+ .model_selector()
+ .map(|selector| {
+ cx.new(|cx| {
+ AcpModelSelectorPopover::new(
+ thread.read(cx).session_id().clone(),
+ selector,
+ PopoverMenuHandle::default(),
+ this.focus_handle(cx),
+ window,
+ cx,
+ )
+ })
+ });
+
this.thread_state = ThreadState::Ready {
thread,
_subscription: [thread_subscription, action_log_subscription],
@@ -2472,6 +2494,12 @@ impl AcpThreadView {
v_flex()
.on_action(cx.listener(Self::expand_message_editor))
+ .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
+ if let Some(model_selector) = this.model_selector.as_ref() {
+ model_selector
+ .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
+ }
+ }))
.p_2()
.gap_2()
.border_t_1()
@@ -2548,7 +2576,12 @@ impl AcpThreadView {
.flex_none()
.justify_between()
.child(self.render_follow_toggle(cx))
- .child(self.render_send_button(cx)),
+ .child(
+ h_flex()
+ .gap_1()
+ .children(self.model_selector.clone())
+ .child(self.render_send_button(cx)),
+ ),
)
.into_any()
}
@@ -916,6 +916,7 @@ impl AgentPanel {
let workspace = self.workspace.clone();
let project = self.project.clone();
let message_history = self.acp_message_history.clone();
+ let fs = self.fs.clone();
const LAST_USED_EXTERNAL_AGENT_KEY: &str = "agent_panel__last_used_external_agent";
@@ -939,7 +940,7 @@ impl AgentPanel {
})
.detach();
- agent.server()
+ agent.server(fs)
}
None => cx
.background_spawn(async move {
@@ -953,7 +954,7 @@ impl AgentPanel {
})
.unwrap_or_default()
.agent
- .server(),
+ .server(fs),
};
this.update_in(cx, |this, window, cx| {
@@ -155,11 +155,11 @@ enum ExternalAgent {
}
impl ExternalAgent {
- pub fn server(&self) -> Rc<dyn agent_servers::AgentServer> {
+ pub fn server(&self, fs: Arc<dyn fs::Fs>) -> 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),
+ ExternalAgent::NativeAgent => Rc::new(agent2::NativeAgentServer::new(fs)),
}
}
}