acp: Beta support for Session Config Options (#45751)
Adds beta support for the ACP draft feature of Session Config Options: https://agentclientprotocol.com/rfds/session-config-options Release Notes: - N/A
This commit is contained in:
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -268,6 +268,7 @@ dependencies = [
|
||||
"client",
|
||||
"collections",
|
||||
"env_logger 0.11.8",
|
||||
"feature_flags",
|
||||
"fs",
|
||||
"futures 0.3.31",
|
||||
"gpui",
|
||||
|
||||
@@ -884,6 +884,7 @@ pub enum AcpThreadEvent {
|
||||
Refusal,
|
||||
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
|
||||
ModeUpdated(acp::SessionModeId),
|
||||
ConfigOptionsUpdated(Vec<acp::SessionConfigOption>),
|
||||
}
|
||||
|
||||
impl EventEmitter<AcpThreadEvent> for AcpThread {}
|
||||
@@ -1193,6 +1194,10 @@ impl AcpThread {
|
||||
current_mode_id,
|
||||
..
|
||||
}) => cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id)),
|
||||
acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
|
||||
config_options,
|
||||
..
|
||||
}) => cx.emit(AcpThreadEvent::ConfigOptionsUpdated(config_options)),
|
||||
_ => {}
|
||||
}
|
||||
Ok(())
|
||||
|
||||
@@ -86,6 +86,14 @@ pub trait AgentConnection {
|
||||
None
|
||||
}
|
||||
|
||||
fn session_config_options(
|
||||
&self,
|
||||
_session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn AgentSessionConfigOptions>> {
|
||||
None
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
}
|
||||
|
||||
@@ -125,6 +133,26 @@ pub trait AgentSessionModes {
|
||||
fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
|
||||
}
|
||||
|
||||
pub trait AgentSessionConfigOptions {
|
||||
/// Get all current config options with their state
|
||||
fn config_options(&self) -> Vec<acp::SessionConfigOption>;
|
||||
|
||||
/// Set a config option value
|
||||
/// Returns the full updated list of config options
|
||||
fn set_config_option(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value: acp::SessionConfigValueId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<acp::SessionConfigOption>>>;
|
||||
|
||||
/// Whenever the config options are updated the receiver will be notified.
|
||||
/// Optional for agents that don't update their config options dynamically.
|
||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug)]
|
||||
pub struct AuthRequired {
|
||||
pub description: Option<String>,
|
||||
|
||||
@@ -21,6 +21,7 @@ acp_tools.workspace = true
|
||||
acp_thread.workspace = true
|
||||
action_log.workspace = true
|
||||
agent-client-protocol.workspace = true
|
||||
feature_flags.workspace = true
|
||||
anyhow.workspace = true
|
||||
async-trait.workspace = true
|
||||
client.workspace = true
|
||||
|
||||
@@ -4,6 +4,7 @@ use action_log::ActionLog;
|
||||
use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
|
||||
use anyhow::anyhow;
|
||||
use collections::HashMap;
|
||||
use feature_flags::{AcpBetaFeatureFlag, FeatureFlagAppExt as _};
|
||||
use futures::AsyncBufReadExt as _;
|
||||
use futures::io::BufReader;
|
||||
use project::Project;
|
||||
@@ -38,6 +39,7 @@ pub struct AcpConnection {
|
||||
agent_capabilities: acp::AgentCapabilities,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
root_dir: PathBuf,
|
||||
// NB: Don't move this into the wait_task, since we need to ensure the process is
|
||||
// killed on drop (setting kill_on_drop on the command seems to not always work).
|
||||
@@ -47,11 +49,29 @@ pub struct AcpConnection {
|
||||
_stderr_task: Task<Result<()>>,
|
||||
}
|
||||
|
||||
struct ConfigOptions {
|
||||
config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
|
||||
tx: Rc<RefCell<watch::Sender<()>>>,
|
||||
rx: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
impl ConfigOptions {
|
||||
fn new(config_options: Rc<RefCell<Vec<acp::SessionConfigOption>>>) -> Self {
|
||||
let (tx, rx) = watch::channel(());
|
||||
Self {
|
||||
config_options,
|
||||
tx: Rc::new(RefCell::new(tx)),
|
||||
rx,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct AcpSession {
|
||||
thread: WeakEntity<AcpThread>,
|
||||
suppress_abort_err: bool,
|
||||
models: Option<Rc<RefCell<acp::SessionModelState>>>,
|
||||
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
|
||||
config_options: Option<ConfigOptions>,
|
||||
}
|
||||
|
||||
pub async fn connect(
|
||||
@@ -60,6 +80,7 @@ pub async fn connect(
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Rc<dyn AgentConnection>> {
|
||||
@@ -69,6 +90,7 @@ pub async fn connect(
|
||||
root_dir,
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
@@ -85,6 +107,7 @@ impl AcpConnection {
|
||||
root_dir: &Path,
|
||||
default_mode: Option<acp::SessionModeId>,
|
||||
default_model: Option<acp::ModelId>,
|
||||
default_config_options: HashMap<String, String>,
|
||||
is_remote: bool,
|
||||
cx: &mut AsyncApp,
|
||||
) -> Result<Self> {
|
||||
@@ -217,6 +240,7 @@ impl AcpConnection {
|
||||
agent_capabilities: response.agent_capabilities,
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
_io_task: io_task,
|
||||
_wait_task: wait_task,
|
||||
_stderr_task: stderr_task,
|
||||
@@ -256,6 +280,7 @@ impl AgentConnection for AcpConnection {
|
||||
let sessions = self.sessions.clone();
|
||||
let default_mode = self.default_mode.clone();
|
||||
let default_model = self.default_model.clone();
|
||||
let default_config_options = self.default_config_options.clone();
|
||||
let cwd = cwd.to_path_buf();
|
||||
let context_server_store = project.read(cx).context_server_store().read(cx);
|
||||
let mcp_servers = if project.read(cx).is_local() {
|
||||
@@ -322,8 +347,21 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
})?;
|
||||
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||
let use_config_options = cx.update(|cx| cx.has_flag::<AcpBetaFeatureFlag>())?;
|
||||
|
||||
// Config options take precedence over legacy modes/models
|
||||
let (modes, models, config_options) = if use_config_options && let Some(opts) = response.config_options {
|
||||
(
|
||||
None,
|
||||
None,
|
||||
Some(Rc::new(RefCell::new(opts))),
|
||||
)
|
||||
} else {
|
||||
// Fall back to legacy modes/models
|
||||
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
|
||||
let models = response.models.map(|models| Rc::new(RefCell::new(models)));
|
||||
(modes, models, None)
|
||||
};
|
||||
|
||||
if let Some(default_mode) = default_mode {
|
||||
if let Some(modes) = modes.as_ref() {
|
||||
@@ -411,6 +449,92 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(config_opts) = config_options.as_ref() {
|
||||
let defaults_to_apply: Vec<_> = {
|
||||
let config_opts_ref = config_opts.borrow();
|
||||
config_opts_ref
|
||||
.iter()
|
||||
.filter_map(|config_option| {
|
||||
let default_value = default_config_options.get(&*config_option.id.0)?;
|
||||
|
||||
let is_valid = match &config_option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => {
|
||||
options.iter().any(|opt| &*opt.value.0 == default_value.as_str())
|
||||
}
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups
|
||||
.iter()
|
||||
.any(|g| g.options.iter().any(|opt| &*opt.value.0 == default_value.as_str())),
|
||||
_ => false,
|
||||
},
|
||||
_ => false,
|
||||
};
|
||||
|
||||
if is_valid {
|
||||
let initial_value = match &config_option.kind {
|
||||
acp::SessionConfigKind::Select(select) => {
|
||||
Some(select.current_value.clone())
|
||||
}
|
||||
_ => None,
|
||||
};
|
||||
Some((config_option.id.clone(), default_value.clone(), initial_value))
|
||||
} else {
|
||||
log::warn!(
|
||||
"`{}` is not a valid value for config option `{}` in {}",
|
||||
default_value,
|
||||
config_option.id.0,
|
||||
name
|
||||
);
|
||||
None
|
||||
}
|
||||
})
|
||||
.collect()
|
||||
};
|
||||
|
||||
for (config_id, default_value, initial_value) in defaults_to_apply {
|
||||
cx.spawn({
|
||||
let default_value_id = acp::SessionConfigValueId::new(default_value.clone());
|
||||
let session_id = response.session_id.clone();
|
||||
let config_id_clone = config_id.clone();
|
||||
let config_opts = config_opts.clone();
|
||||
let conn = conn.clone();
|
||||
async move |_| {
|
||||
let result = conn
|
||||
.set_session_config_option(
|
||||
acp::SetSessionConfigOptionRequest::new(
|
||||
session_id,
|
||||
config_id_clone.clone(),
|
||||
default_value_id,
|
||||
),
|
||||
)
|
||||
.await
|
||||
.log_err();
|
||||
|
||||
if result.is_none() {
|
||||
if let Some(initial) = initial_value {
|
||||
let mut opts = config_opts.borrow_mut();
|
||||
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id_clone) {
|
||||
if let acp::SessionConfigKind::Select(select) =
|
||||
&mut opt.kind
|
||||
{
|
||||
select.current_value = initial;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
let mut opts = config_opts.borrow_mut();
|
||||
if let Some(opt) = opts.iter_mut().find(|o| o.id == config_id) {
|
||||
if let acp::SessionConfigKind::Select(select) = &mut opt.kind {
|
||||
select.current_value = acp::SessionConfigValueId::new(default_value);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
let session_id = response.session_id;
|
||||
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
|
||||
let thread = cx.new(|cx| {
|
||||
@@ -432,6 +556,7 @@ impl AgentConnection for AcpConnection {
|
||||
suppress_abort_err: false,
|
||||
session_modes: modes,
|
||||
models,
|
||||
config_options: config_options.map(|opts| ConfigOptions::new(opts))
|
||||
};
|
||||
sessions.borrow_mut().insert(session_id, session);
|
||||
|
||||
@@ -567,6 +692,25 @@ impl AgentConnection for AcpConnection {
|
||||
}
|
||||
}
|
||||
|
||||
fn session_config_options(
|
||||
&self,
|
||||
session_id: &acp::SessionId,
|
||||
_cx: &App,
|
||||
) -> Option<Rc<dyn acp_thread::AgentSessionConfigOptions>> {
|
||||
let sessions = self.sessions.borrow();
|
||||
let session = sessions.get(session_id)?;
|
||||
|
||||
let config_opts = session.config_options.as_ref()?;
|
||||
|
||||
Some(Rc::new(AcpSessionConfigOptions {
|
||||
session_id: session_id.clone(),
|
||||
connection: self.connection.clone(),
|
||||
state: config_opts.config_options.clone(),
|
||||
watch_tx: config_opts.tx.clone(),
|
||||
watch_rx: config_opts.rx.clone(),
|
||||
}) as _)
|
||||
}
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
|
||||
self
|
||||
}
|
||||
@@ -685,6 +829,49 @@ impl acp_thread::AgentModelSelector for AcpModelSelector {
|
||||
}
|
||||
}
|
||||
|
||||
struct AcpSessionConfigOptions {
|
||||
session_id: acp::SessionId,
|
||||
connection: Rc<acp::ClientSideConnection>,
|
||||
state: Rc<RefCell<Vec<acp::SessionConfigOption>>>,
|
||||
watch_tx: Rc<RefCell<watch::Sender<()>>>,
|
||||
watch_rx: watch::Receiver<()>,
|
||||
}
|
||||
|
||||
impl acp_thread::AgentSessionConfigOptions for AcpSessionConfigOptions {
|
||||
fn config_options(&self) -> Vec<acp::SessionConfigOption> {
|
||||
self.state.borrow().clone()
|
||||
}
|
||||
|
||||
fn set_config_option(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value: acp::SessionConfigValueId,
|
||||
cx: &mut App,
|
||||
) -> Task<Result<Vec<acp::SessionConfigOption>>> {
|
||||
let connection = self.connection.clone();
|
||||
let session_id = self.session_id.clone();
|
||||
let state = self.state.clone();
|
||||
|
||||
let watch_tx = self.watch_tx.clone();
|
||||
|
||||
cx.foreground_executor().spawn(async move {
|
||||
let response = connection
|
||||
.set_session_config_option(acp::SetSessionConfigOptionRequest::new(
|
||||
session_id, config_id, value,
|
||||
))
|
||||
.await?;
|
||||
|
||||
*state.borrow_mut() = response.config_options.clone();
|
||||
watch_tx.borrow_mut().send(()).ok();
|
||||
Ok(response.config_options)
|
||||
})
|
||||
}
|
||||
|
||||
fn watch(&self, _cx: &mut App) -> Option<watch::Receiver<()>> {
|
||||
Some(self.watch_rx.clone())
|
||||
}
|
||||
}
|
||||
|
||||
struct ClientDelegate {
|
||||
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
|
||||
cx: AsyncApp,
|
||||
@@ -778,6 +965,21 @@ impl acp::Client for ClientDelegate {
|
||||
}
|
||||
}
|
||||
|
||||
if let acp::SessionUpdate::ConfigOptionUpdate(acp::ConfigOptionUpdate {
|
||||
config_options,
|
||||
..
|
||||
}) = ¬ification.update
|
||||
{
|
||||
if let Some(opts) = &session.config_options {
|
||||
*opts.config_options.borrow_mut() = config_options.clone();
|
||||
opts.tx.borrow_mut().send(()).ok();
|
||||
} else {
|
||||
log::error!(
|
||||
"Got a `ConfigOptionUpdate` notification, but the agent didn't specify `config_options` during session setup."
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
// Clone so we can inspect meta both before and after handing off to the thread
|
||||
let update_clone = notification.update.clone();
|
||||
|
||||
|
||||
@@ -4,15 +4,13 @@ mod codex;
|
||||
mod custom;
|
||||
mod gemini;
|
||||
|
||||
use collections::HashSet;
|
||||
|
||||
#[cfg(any(test, feature = "test-support"))]
|
||||
pub mod e2e_tests;
|
||||
|
||||
pub use claude::*;
|
||||
use client::ProxySettings;
|
||||
pub use codex::*;
|
||||
use collections::HashMap;
|
||||
use collections::{HashMap, HashSet};
|
||||
pub use custom::*;
|
||||
use fs::Fs;
|
||||
pub use gemini::*;
|
||||
@@ -67,7 +65,7 @@ pub trait AgentServer: Send {
|
||||
|
||||
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
|
||||
|
||||
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
fn default_mode(&self, _cx: &App) -> Option<agent_client_protocol::SessionModeId> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -79,7 +77,7 @@ pub trait AgentServer: Send {
|
||||
) {
|
||||
}
|
||||
|
||||
fn default_model(&self, _cx: &mut App) -> Option<agent_client_protocol::ModelId> {
|
||||
fn default_model(&self, _cx: &App) -> Option<agent_client_protocol::ModelId> {
|
||||
None
|
||||
}
|
||||
|
||||
@@ -95,6 +93,37 @@ pub trait AgentServer: Send {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn default_config_option(&self, _config_id: &str, _cx: &App) -> Option<String> {
|
||||
None
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
_config_id: &str,
|
||||
_value_id: Option<&str>,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &mut App,
|
||||
) {
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
_config_id: &agent_client_protocol::SessionConfigId,
|
||||
_cx: &mut App,
|
||||
) -> HashSet<agent_client_protocol::SessionConfigValueId> {
|
||||
HashSet::default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
_config_id: agent_client_protocol::SessionConfigId,
|
||||
_value_id: agent_client_protocol::SessionConfigValueId,
|
||||
_should_be_favorite: bool,
|
||||
_fs: Arc<dyn Fs>,
|
||||
_cx: &App,
|
||||
) {
|
||||
}
|
||||
|
||||
fn toggle_favorite_model(
|
||||
&self,
|
||||
_model_id: agent_client_protocol::ModelId,
|
||||
|
||||
@@ -31,7 +31,7 @@ impl AgentServer for ClaudeCode {
|
||||
ui::IconName::AiClaude
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
@@ -52,7 +52,7 @@ impl AgentServer for ClaudeCode {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
@@ -115,6 +115,97 @@ impl AgentServer for ClaudeCode {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_options.get(config_id).cloned())
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let config_options = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.default_config_options;
|
||||
|
||||
if let Some(value) = value_id.clone() {
|
||||
config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
config_options.remove(&config_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).claude.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorites = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.claude
|
||||
.get_or_insert_default()
|
||||
.favorite_config_option_values;
|
||||
|
||||
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorites.remove(&config_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -128,6 +219,14 @@ impl AgentServer for ClaudeCode {
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.claude
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
@@ -150,6 +249,7 @@ impl AgentServer for ClaudeCode {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -32,7 +32,7 @@ impl AgentServer for Codex {
|
||||
ui::IconName::AiOpenAi
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
@@ -53,7 +53,7 @@ impl AgentServer for Codex {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
@@ -116,6 +116,97 @@ impl AgentServer for Codex {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_options.get(config_id).cloned())
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let config_options = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.default_config_options;
|
||||
|
||||
if let Some(value) = value_id.clone() {
|
||||
config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
config_options.remove(&config_id);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings.get::<AllAgentServersSettings>(None).codex.clone()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values.get(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let favorites = &mut settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.codex
|
||||
.get_or_insert_default()
|
||||
.favorite_config_option_values;
|
||||
|
||||
let entry = favorites.entry(config_id.clone()).or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorites.remove(&config_id);
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -129,6 +220,14 @@ impl AgentServer for Codex {
|
||||
let extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.codex
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
let (command, root_dir, login) = store
|
||||
@@ -152,6 +251,7 @@ impl AgentServer for Codex {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -30,7 +30,7 @@ impl AgentServer for CustomAgentServer {
|
||||
IconName::Terminal
|
||||
}
|
||||
|
||||
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
|
||||
fn default_mode(&self, cx: &App) -> Option<acp::SessionModeId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
@@ -44,6 +44,86 @@ impl AgentServer for CustomAgentServer {
|
||||
.and_then(|s| s.default_mode().map(acp::SessionModeId::new))
|
||||
}
|
||||
|
||||
fn favorite_config_option_value_ids(
|
||||
&self,
|
||||
config_id: &acp::SessionConfigId,
|
||||
cx: &mut App,
|
||||
) -> HashSet<acp::SessionConfigValueId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.favorite_config_option_values(config_id.0.as_ref()))
|
||||
.map(|values| {
|
||||
values
|
||||
.iter()
|
||||
.cloned()
|
||||
.map(acp::SessionConfigValueId::new)
|
||||
.collect()
|
||||
})
|
||||
.unwrap_or_default()
|
||||
}
|
||||
|
||||
fn toggle_favorite_config_option_value(
|
||||
&self,
|
||||
config_id: acp::SessionConfigId,
|
||||
value_id: acp::SessionConfigValueId,
|
||||
should_be_favorite: bool,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &App,
|
||||
) {
|
||||
let name = self.name();
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.to_string();
|
||||
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let settings = settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.custom
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
} => {
|
||||
let entry = favorite_config_option_values
|
||||
.entry(config_id.clone())
|
||||
.or_insert_with(Vec::new);
|
||||
|
||||
if should_be_favorite {
|
||||
if !entry.iter().any(|v| v == &value_id) {
|
||||
entry.push(value_id.clone());
|
||||
}
|
||||
} else {
|
||||
entry.retain(|v| v != &value_id);
|
||||
if entry.is_empty() {
|
||||
favorite_config_option_values.remove(&config_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
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, _| {
|
||||
@@ -56,6 +136,8 @@ impl AgentServer for CustomAgentServer {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -67,7 +149,7 @@ impl AgentServer for CustomAgentServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_model(&self, cx: &mut App) -> Option<acp::ModelId> {
|
||||
fn default_model(&self, cx: &App) -> Option<acp::ModelId> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
@@ -93,6 +175,8 @@ impl AgentServer for CustomAgentServer {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
@@ -142,6 +226,8 @@ impl AgentServer for CustomAgentServer {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
let favorite_models = match settings {
|
||||
@@ -164,6 +250,63 @@ impl AgentServer for CustomAgentServer {
|
||||
});
|
||||
}
|
||||
|
||||
fn default_config_option(&self, config_id: &str, cx: &App) -> Option<String> {
|
||||
let settings = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.cloned()
|
||||
});
|
||||
|
||||
settings
|
||||
.as_ref()
|
||||
.and_then(|s| s.default_config_option(config_id).map(|s| s.to_string()))
|
||||
}
|
||||
|
||||
fn set_default_config_option(
|
||||
&self,
|
||||
config_id: &str,
|
||||
value_id: Option<&str>,
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &mut App,
|
||||
) {
|
||||
let name = self.name();
|
||||
let config_id = config_id.to_string();
|
||||
let value_id = value_id.map(|s| s.to_string());
|
||||
update_settings_file(fs, cx, move |settings, _| {
|
||||
let settings = settings
|
||||
.agent_servers
|
||||
.get_or_insert_default()
|
||||
.custom
|
||||
.entry(name.clone())
|
||||
.or_insert_with(|| settings::CustomAgentServerSettings::Extension {
|
||||
default_model: None,
|
||||
default_mode: None,
|
||||
favorite_models: Vec::new(),
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
});
|
||||
|
||||
match settings {
|
||||
settings::CustomAgentServerSettings::Custom {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| settings::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
} => {
|
||||
if let Some(value) = value_id.clone() {
|
||||
default_config_options.insert(config_id.clone(), value);
|
||||
} else {
|
||||
default_config_options.remove(&config_id);
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
fn connect(
|
||||
&self,
|
||||
root_dir: Option<&Path>,
|
||||
@@ -175,6 +318,23 @@ impl AgentServer for CustomAgentServer {
|
||||
let is_remote = delegate.project.read(cx).is_via_remote_server();
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.custom
|
||||
.get(&self.name())
|
||||
.map(|s| match s {
|
||||
project::agent_server_store::CustomAgentServerSettings::Custom {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| project::agent_server_store::CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
} => default_config_options.clone(),
|
||||
})
|
||||
.unwrap_or_default()
|
||||
});
|
||||
let store = delegate.store.downgrade();
|
||||
let extra_env = load_proxy_env(cx);
|
||||
cx.spawn(async move |cx| {
|
||||
@@ -200,6 +360,7 @@ impl AgentServer for CustomAgentServer {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -455,22 +455,12 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
|
||||
project::agent_server_store::AllAgentServersSettings {
|
||||
claude: Some(BuiltinAgentServerSettings {
|
||||
path: Some("claude-code-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
..Default::default()
|
||||
}),
|
||||
gemini: Some(crate::gemini::tests::local_command().into()),
|
||||
codex: Some(BuiltinAgentServerSettings {
|
||||
path: Some("codex-acp".into()),
|
||||
args: None,
|
||||
env: None,
|
||||
ignore_system_version: None,
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
..Default::default()
|
||||
}),
|
||||
custom: collections::HashMap::default(),
|
||||
},
|
||||
|
||||
@@ -4,9 +4,10 @@ use std::{any::Any, path::Path};
|
||||
use crate::{AgentServer, AgentServerDelegate, load_proxy_env};
|
||||
use acp_thread::AgentConnection;
|
||||
use anyhow::{Context as _, Result};
|
||||
use gpui::{App, SharedString, Task};
|
||||
use gpui::{App, AppContext as _, SharedString, Task};
|
||||
use language_models::provider::google::GoogleLanguageModelProvider;
|
||||
use project::agent_server_store::GEMINI_NAME;
|
||||
use project::agent_server_store::{AllAgentServersSettings, GEMINI_NAME};
|
||||
use settings::SettingsStore;
|
||||
|
||||
#[derive(Clone)]
|
||||
pub struct Gemini;
|
||||
@@ -33,6 +34,14 @@ impl AgentServer for Gemini {
|
||||
let mut extra_env = load_proxy_env(cx);
|
||||
let default_mode = self.default_mode(cx);
|
||||
let default_model = self.default_model(cx);
|
||||
let default_config_options = cx.read_global(|settings: &SettingsStore, _| {
|
||||
settings
|
||||
.get::<AllAgentServersSettings>(None)
|
||||
.gemini
|
||||
.as_ref()
|
||||
.map(|s| s.default_config_options.clone())
|
||||
.unwrap_or_default()
|
||||
});
|
||||
|
||||
cx.spawn(async move |cx| {
|
||||
extra_env.insert("SURFACE".to_owned(), "zed".to_owned());
|
||||
@@ -65,6 +74,7 @@ impl AgentServer for Gemini {
|
||||
root_dir.as_ref(),
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
is_remote,
|
||||
cx,
|
||||
)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
mod config_options;
|
||||
mod entry_view_state;
|
||||
mod message_editor;
|
||||
mod mode_selector;
|
||||
|
||||
772
crates/agent_ui/src/acp/config_options.rs
Normal file
772
crates/agent_ui/src/acp/config_options.rs
Normal file
@@ -0,0 +1,772 @@
|
||||
use std::{cmp::Reverse, rc::Rc, sync::Arc};
|
||||
|
||||
use acp_thread::AgentSessionConfigOptions;
|
||||
use agent_client_protocol as acp;
|
||||
use agent_servers::AgentServer;
|
||||
use collections::HashSet;
|
||||
use fs::Fs;
|
||||
use fuzzy::StringMatchCandidate;
|
||||
use gpui::{
|
||||
BackgroundExecutor, Context, DismissEvent, Entity, Subscription, Task, Window, prelude::*,
|
||||
};
|
||||
use ordered_float::OrderedFloat;
|
||||
use picker::popover_menu::PickerPopoverMenu;
|
||||
use picker::{Picker, PickerDelegate};
|
||||
use settings::SettingsStore;
|
||||
use ui::{
|
||||
ElevationIndex, IconButton, ListItem, ListItemSpacing, PopoverMenuHandle, Tooltip, prelude::*,
|
||||
};
|
||||
use util::ResultExt as _;
|
||||
|
||||
use crate::ui::HoldForDefault;
|
||||
|
||||
const PICKER_THRESHOLD: usize = 5;
|
||||
|
||||
pub struct ConfigOptionsView {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
selectors: Vec<Entity<ConfigOptionSelector>>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
config_option_ids: Vec<acp::SessionConfigId>,
|
||||
_refresh_task: Task<()>,
|
||||
}
|
||||
|
||||
impl ConfigOptionsView {
|
||||
pub fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let selectors = Self::build_selectors(&config_options, &agent_server, &fs, window, cx);
|
||||
let config_option_ids = Self::config_option_ids(&config_options);
|
||||
|
||||
let rx = config_options.watch(cx);
|
||||
let refresh_task = cx.spawn_in(window, async move |this, cx| {
|
||||
if let Some(mut rx) = rx {
|
||||
while let Ok(()) = rx.recv().await {
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.refresh_selectors_if_needed(window, cx);
|
||||
cx.notify();
|
||||
})
|
||||
.log_err();
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
selectors,
|
||||
agent_server,
|
||||
fs,
|
||||
config_option_ids,
|
||||
_refresh_task: refresh_task,
|
||||
}
|
||||
}
|
||||
|
||||
fn config_option_ids(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
) -> Vec<acp::SessionConfigId> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.map(|option| option.id)
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn refresh_selectors_if_needed(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
let current_ids = Self::config_option_ids(&self.config_options);
|
||||
if current_ids != self.config_option_ids {
|
||||
self.config_option_ids = current_ids;
|
||||
self.rebuild_selectors(window, cx);
|
||||
}
|
||||
}
|
||||
|
||||
fn rebuild_selectors(&mut self, window: &mut Window, cx: &mut Context<Self>) {
|
||||
self.selectors = Self::build_selectors(
|
||||
&self.config_options,
|
||||
&self.agent_server,
|
||||
&self.fs,
|
||||
window,
|
||||
cx,
|
||||
);
|
||||
cx.notify();
|
||||
}
|
||||
|
||||
fn build_selectors(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
agent_server: &Rc<dyn AgentServer>,
|
||||
fs: &Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Vec<Entity<ConfigOptionSelector>> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.map(|option| {
|
||||
let config_options = config_options.clone();
|
||||
let agent_server = agent_server.clone();
|
||||
let fs = fs.clone();
|
||||
cx.new(|cx| {
|
||||
ConfigOptionSelector::new(
|
||||
config_options,
|
||||
option.id.clone(),
|
||||
agent_server,
|
||||
fs,
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
})
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigOptionsView {
|
||||
fn render(&mut self, _window: &mut Window, _cx: &mut Context<Self>) -> impl IntoElement {
|
||||
if self.selectors.is_empty() {
|
||||
return div().into_any_element();
|
||||
}
|
||||
|
||||
h_flex()
|
||||
.gap_1()
|
||||
.children(self.selectors.iter().cloned())
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
struct ConfigOptionSelector {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
picker_handle: PopoverMenuHandle<Picker<ConfigOptionPickerDelegate>>,
|
||||
picker: Entity<Picker<ConfigOptionPickerDelegate>>,
|
||||
setting_value: bool,
|
||||
}
|
||||
|
||||
impl ConfigOptionSelector {
|
||||
pub fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Self>,
|
||||
) -> Self {
|
||||
let option_count = config_options
|
||||
.config_options()
|
||||
.iter()
|
||||
.find(|opt| opt.id == config_id)
|
||||
.map(count_config_options)
|
||||
.unwrap_or(0);
|
||||
|
||||
let is_searchable = option_count >= PICKER_THRESHOLD;
|
||||
|
||||
let picker = {
|
||||
let config_options = config_options.clone();
|
||||
let config_id = config_id.clone();
|
||||
let agent_server = agent_server.clone();
|
||||
let fs = fs.clone();
|
||||
cx.new(move |picker_cx| {
|
||||
let delegate = ConfigOptionPickerDelegate::new(
|
||||
config_options,
|
||||
config_id,
|
||||
agent_server,
|
||||
fs,
|
||||
window,
|
||||
picker_cx,
|
||||
);
|
||||
|
||||
if is_searchable {
|
||||
Picker::list(delegate, window, picker_cx)
|
||||
} else {
|
||||
Picker::nonsearchable_list(delegate, window, picker_cx)
|
||||
}
|
||||
.show_scrollbar(true)
|
||||
.width(rems(20.))
|
||||
.max_height(Some(rems(20.).into()))
|
||||
})
|
||||
};
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
config_id,
|
||||
picker_handle: PopoverMenuHandle::default(),
|
||||
picker,
|
||||
setting_value: false,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_option(&self) -> Option<acp::SessionConfigOption> {
|
||||
self.config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| opt.id == self.config_id)
|
||||
}
|
||||
|
||||
fn current_value_name(&self) -> String {
|
||||
let Some(option) = self.current_option() else {
|
||||
return "Unknown".to_string();
|
||||
};
|
||||
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => {
|
||||
find_option_name(&select.options, &select.current_value)
|
||||
.unwrap_or_else(|| "Unknown".to_string())
|
||||
}
|
||||
_ => "Unknown".to_string(),
|
||||
}
|
||||
}
|
||||
|
||||
fn render_trigger_button(&self, _window: &mut Window, _cx: &mut Context<Self>) -> Button {
|
||||
let Some(option) = self.current_option() else {
|
||||
return Button::new("config-option-trigger", "Unknown")
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.disabled(true);
|
||||
};
|
||||
|
||||
let icon = if self.picker_handle.is_deployed() {
|
||||
IconName::ChevronUp
|
||||
} else {
|
||||
IconName::ChevronDown
|
||||
};
|
||||
|
||||
Button::new(
|
||||
ElementId::Name(format!("config-option-{}", option.id.0).into()),
|
||||
self.current_value_name(),
|
||||
)
|
||||
.label_size(LabelSize::Small)
|
||||
.color(Color::Muted)
|
||||
.icon(icon)
|
||||
.icon_size(IconSize::XSmall)
|
||||
.icon_position(IconPosition::End)
|
||||
.icon_color(Color::Muted)
|
||||
.disabled(self.setting_value)
|
||||
}
|
||||
}
|
||||
|
||||
impl Render for ConfigOptionSelector {
|
||||
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
||||
let Some(option) = self.current_option() else {
|
||||
return div().into_any_element();
|
||||
};
|
||||
|
||||
let trigger_button = self.render_trigger_button(window, cx);
|
||||
|
||||
let option_name = option.name.clone();
|
||||
let option_description: Option<SharedString> = option.description.map(Into::into);
|
||||
|
||||
let tooltip = Tooltip::element(move |_window, _cx| {
|
||||
let mut content = v_flex().gap_1().child(Label::new(option_name.clone()));
|
||||
if let Some(desc) = option_description.as_ref() {
|
||||
content = content.child(
|
||||
Label::new(desc.clone())
|
||||
.size(LabelSize::Small)
|
||||
.color(Color::Muted),
|
||||
);
|
||||
}
|
||||
content.into_any()
|
||||
});
|
||||
|
||||
PickerPopoverMenu::new(
|
||||
self.picker.clone(),
|
||||
trigger_button,
|
||||
tooltip,
|
||||
gpui::Corner::BottomRight,
|
||||
cx,
|
||||
)
|
||||
.with_handle(self.picker_handle.clone())
|
||||
.render(window, cx)
|
||||
.into_any_element()
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
enum ConfigOptionPickerEntry {
|
||||
Separator(SharedString),
|
||||
Option(ConfigOptionValue),
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct ConfigOptionValue {
|
||||
value: acp::SessionConfigValueId,
|
||||
name: String,
|
||||
description: Option<String>,
|
||||
group: Option<String>,
|
||||
}
|
||||
|
||||
struct ConfigOptionPickerDelegate {
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
filtered_entries: Vec<ConfigOptionPickerEntry>,
|
||||
all_options: Vec<ConfigOptionValue>,
|
||||
selected_index: usize,
|
||||
selected_description: Option<(usize, SharedString, bool)>,
|
||||
favorites: HashSet<acp::SessionConfigValueId>,
|
||||
_settings_subscription: Subscription,
|
||||
}
|
||||
|
||||
impl ConfigOptionPickerDelegate {
|
||||
fn new(
|
||||
config_options: Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: acp::SessionConfigId,
|
||||
agent_server: Rc<dyn AgentServer>,
|
||||
fs: Arc<dyn Fs>,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Self {
|
||||
let favorites = agent_server.favorite_config_option_value_ids(&config_id, cx);
|
||||
|
||||
let all_options = extract_options(&config_options, &config_id);
|
||||
let filtered_entries = options_to_picker_entries(&all_options, &favorites);
|
||||
|
||||
let current_value = get_current_value(&config_options, &config_id);
|
||||
let selected_index = current_value
|
||||
.and_then(|current| {
|
||||
filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
|
||||
})
|
||||
})
|
||||
.unwrap_or(0);
|
||||
|
||||
let agent_server_for_subscription = agent_server.clone();
|
||||
let config_id_for_subscription = config_id.clone();
|
||||
let settings_subscription =
|
||||
cx.observe_global_in::<SettingsStore>(window, move |picker, window, cx| {
|
||||
let new_favorites = agent_server_for_subscription
|
||||
.favorite_config_option_value_ids(&config_id_for_subscription, cx);
|
||||
if new_favorites != picker.delegate.favorites {
|
||||
picker.delegate.favorites = new_favorites;
|
||||
picker.refresh(window, cx);
|
||||
}
|
||||
});
|
||||
|
||||
cx.notify();
|
||||
|
||||
Self {
|
||||
config_options,
|
||||
config_id,
|
||||
agent_server,
|
||||
fs,
|
||||
filtered_entries,
|
||||
all_options,
|
||||
selected_index,
|
||||
selected_description: None,
|
||||
favorites,
|
||||
_settings_subscription: settings_subscription,
|
||||
}
|
||||
}
|
||||
|
||||
fn current_value(&self) -> Option<acp::SessionConfigValueId> {
|
||||
get_current_value(&self.config_options, &self.config_id)
|
||||
}
|
||||
}
|
||||
|
||||
impl PickerDelegate for ConfigOptionPickerDelegate {
|
||||
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(ConfigOptionPickerEntry::Option(_)) => true,
|
||||
Some(ConfigOptionPickerEntry::Separator(_)) | None => false,
|
||||
}
|
||||
}
|
||||
|
||||
fn placeholder_text(&self, _window: &mut Window, _cx: &mut App) -> Arc<str> {
|
||||
"Select an option…".into()
|
||||
}
|
||||
|
||||
fn update_matches(
|
||||
&mut self,
|
||||
query: String,
|
||||
window: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Task<()> {
|
||||
let all_options = self.all_options.clone();
|
||||
|
||||
cx.spawn_in(window, async move |this, cx| {
|
||||
let filtered_options = match this
|
||||
.read_with(cx, |_, cx| {
|
||||
if query.is_empty() {
|
||||
None
|
||||
} else {
|
||||
Some((all_options.clone(), query.clone(), cx.background_executor().clone()))
|
||||
}
|
||||
})
|
||||
.ok()
|
||||
.flatten()
|
||||
{
|
||||
Some((options, q, executor)) => fuzzy_search_options(options, &q, executor).await,
|
||||
None => all_options,
|
||||
};
|
||||
|
||||
this.update_in(cx, |this, window, cx| {
|
||||
this.delegate.filtered_entries =
|
||||
options_to_picker_entries(&filtered_options, &this.delegate.favorites);
|
||||
|
||||
let current_value = this.delegate.current_value();
|
||||
let new_index = current_value
|
||||
.and_then(|current| {
|
||||
this.delegate.filtered_entries.iter().position(|entry| {
|
||||
matches!(entry, ConfigOptionPickerEntry::Option(opt) if opt.value == current)
|
||||
})
|
||||
})
|
||||
.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(ConfigOptionPickerEntry::Option(option)) =
|
||||
self.filtered_entries.get(self.selected_index)
|
||||
{
|
||||
if window.modifiers().secondary() {
|
||||
let default_value = self
|
||||
.agent_server
|
||||
.default_config_option(self.config_id.0.as_ref(), cx);
|
||||
let is_default = default_value.as_deref() == Some(&*option.value.0);
|
||||
|
||||
self.agent_server.set_default_config_option(
|
||||
self.config_id.0.as_ref(),
|
||||
if is_default {
|
||||
None
|
||||
} else {
|
||||
Some(option.value.0.as_ref())
|
||||
},
|
||||
self.fs.clone(),
|
||||
cx,
|
||||
);
|
||||
}
|
||||
|
||||
let task = self.config_options.set_config_option(
|
||||
self.config_id.clone(),
|
||||
option.value.clone(),
|
||||
cx,
|
||||
);
|
||||
|
||||
cx.spawn(async move |_, _| {
|
||||
if let Err(err) = task.await {
|
||||
log::error!("Failed to set config option: {:?}", err);
|
||||
}
|
||||
})
|
||||
.detach();
|
||||
|
||||
cx.emit(DismissEvent);
|
||||
}
|
||||
}
|
||||
|
||||
fn dismissed(&mut self, window: &mut Window, cx: &mut Context<Picker<Self>>) {
|
||||
cx.defer_in(window, |picker, window, cx| {
|
||||
picker.set_query("", window, cx);
|
||||
});
|
||||
}
|
||||
|
||||
fn render_match(
|
||||
&self,
|
||||
ix: usize,
|
||||
selected: bool,
|
||||
_: &mut Window,
|
||||
cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<Self::ListItem> {
|
||||
match self.filtered_entries.get(ix)? {
|
||||
ConfigOptionPickerEntry::Separator(title) => Some(
|
||||
div()
|
||||
.when(ix > 0, |this| this.mt_1())
|
||||
.child(
|
||||
div()
|
||||
.px_2()
|
||||
.py_1()
|
||||
.text_xs()
|
||||
.text_color(cx.theme().colors().text_muted)
|
||||
.child(title.clone()),
|
||||
)
|
||||
.into_any_element(),
|
||||
),
|
||||
ConfigOptionPickerEntry::Option(option) => {
|
||||
let current_value = self.current_value();
|
||||
let is_selected = current_value.as_ref() == Some(&option.value);
|
||||
|
||||
let default_value = self
|
||||
.agent_server
|
||||
.default_config_option(self.config_id.0.as_ref(), cx);
|
||||
let is_default = default_value.as_deref() == Some(&*option.value.0);
|
||||
|
||||
let is_favorite = self.favorites.contains(&option.value);
|
||||
|
||||
let option_name = option.name.clone();
|
||||
let description = option.description.clone();
|
||||
|
||||
Some(
|
||||
div()
|
||||
.id(("config-option-picker-item", ix))
|
||||
.when_some(description, |this, desc| {
|
||||
let desc: SharedString = desc.into();
|
||||
this.on_hover(cx.listener(move |menu, hovered, _, cx| {
|
||||
if *hovered {
|
||||
menu.delegate.selected_description =
|
||||
Some((ix, desc.clone(), is_default));
|
||||
} else if matches!(menu.delegate.selected_description, Some((id, _, _)) if id == ix)
|
||||
{
|
||||
menu.delegate.selected_description = None;
|
||||
}
|
||||
cx.notify();
|
||||
}))
|
||||
})
|
||||
.child(
|
||||
ListItem::new(ix)
|
||||
.inset(true)
|
||||
.spacing(ListItemSpacing::Sparse)
|
||||
.toggle_state(selected)
|
||||
.child(h_flex().w_full().child(Label::new(option_name).truncate()))
|
||||
.end_slot(div().pr_2().when(is_selected, |this| {
|
||||
this.child(Icon::new(IconName::Check).color(Color::Accent))
|
||||
}))
|
||||
.end_hover_slot(div().pr_1p5().child({
|
||||
let (icon, color, tooltip) = if is_favorite {
|
||||
(IconName::StarFilled, Color::Accent, "Unfavorite")
|
||||
} else {
|
||||
(IconName::Star, Color::Default, "Favorite")
|
||||
};
|
||||
|
||||
let config_id = self.config_id.clone();
|
||||
let value_id = option.value.clone();
|
||||
let agent_server = self.agent_server.clone();
|
||||
let fs = self.fs.clone();
|
||||
|
||||
IconButton::new(("toggle-favorite-config-option", ix), icon)
|
||||
.layer(ElevationIndex::ElevatedSurface)
|
||||
.icon_color(color)
|
||||
.icon_size(IconSize::Small)
|
||||
.tooltip(Tooltip::text(tooltip))
|
||||
.on_click(move |_, _, cx| {
|
||||
agent_server.toggle_favorite_config_option_value(
|
||||
config_id.clone(),
|
||||
value_id.clone(),
|
||||
!is_favorite,
|
||||
fs.clone(),
|
||||
cx,
|
||||
);
|
||||
})
|
||||
})),
|
||||
)
|
||||
.into_any_element(),
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
fn documentation_aside(
|
||||
&self,
|
||||
_window: &mut Window,
|
||||
_cx: &mut Context<Picker<Self>>,
|
||||
) -> Option<ui::DocumentationAside> {
|
||||
self.selected_description
|
||||
.as_ref()
|
||||
.map(|(_, description, is_default)| {
|
||||
let description = description.clone();
|
||||
let is_default = *is_default;
|
||||
|
||||
ui::DocumentationAside::new(
|
||||
ui::DocumentationSide::Left,
|
||||
ui::DocumentationEdge::Top,
|
||||
Rc::new(move |_| {
|
||||
v_flex()
|
||||
.gap_1()
|
||||
.child(Label::new(description.clone()))
|
||||
.child(HoldForDefault::new(is_default))
|
||||
.into_any_element()
|
||||
}),
|
||||
)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
fn extract_options(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: &acp::SessionConfigId,
|
||||
) -> Vec<ConfigOptionValue> {
|
||||
let Some(option) = config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| &opt.id == config_id)
|
||||
else {
|
||||
return Vec::new();
|
||||
};
|
||||
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => options
|
||||
.iter()
|
||||
.map(|opt| ConfigOptionValue {
|
||||
value: opt.value.clone(),
|
||||
name: opt.name.clone(),
|
||||
description: opt.description.clone(),
|
||||
group: None,
|
||||
})
|
||||
.collect(),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups
|
||||
.iter()
|
||||
.flat_map(|group| {
|
||||
group.options.iter().map(|opt| ConfigOptionValue {
|
||||
value: opt.value.clone(),
|
||||
name: opt.name.clone(),
|
||||
description: opt.description.clone(),
|
||||
group: Some(group.name.clone()),
|
||||
})
|
||||
})
|
||||
.collect(),
|
||||
_ => Vec::new(),
|
||||
},
|
||||
_ => Vec::new(),
|
||||
}
|
||||
}
|
||||
|
||||
fn get_current_value(
|
||||
config_options: &Rc<dyn AgentSessionConfigOptions>,
|
||||
config_id: &acp::SessionConfigId,
|
||||
) -> Option<acp::SessionConfigValueId> {
|
||||
config_options
|
||||
.config_options()
|
||||
.into_iter()
|
||||
.find(|opt| &opt.id == config_id)
|
||||
.and_then(|opt| match &opt.kind {
|
||||
acp::SessionConfigKind::Select(select) => Some(select.current_value.clone()),
|
||||
_ => None,
|
||||
})
|
||||
}
|
||||
|
||||
fn options_to_picker_entries(
|
||||
options: &[ConfigOptionValue],
|
||||
favorites: &HashSet<acp::SessionConfigValueId>,
|
||||
) -> Vec<ConfigOptionPickerEntry> {
|
||||
let mut entries = Vec::new();
|
||||
|
||||
let mut favorite_options = Vec::new();
|
||||
|
||||
for option in options {
|
||||
if favorites.contains(&option.value) {
|
||||
favorite_options.push(option.clone());
|
||||
}
|
||||
}
|
||||
|
||||
if !favorite_options.is_empty() {
|
||||
entries.push(ConfigOptionPickerEntry::Separator("Favorites".into()));
|
||||
for option in favorite_options {
|
||||
entries.push(ConfigOptionPickerEntry::Option(option));
|
||||
}
|
||||
|
||||
// If the remaining list would start ungrouped (group == None), insert a separator so
|
||||
// Favorites doesn't visually run into the main list.
|
||||
if let Some(option) = options.first()
|
||||
&& option.group.is_none()
|
||||
{
|
||||
entries.push(ConfigOptionPickerEntry::Separator("All Options".into()));
|
||||
}
|
||||
}
|
||||
|
||||
let mut current_group: Option<String> = None;
|
||||
for option in options {
|
||||
if option.group != current_group {
|
||||
if let Some(group_name) = &option.group {
|
||||
entries.push(ConfigOptionPickerEntry::Separator(
|
||||
group_name.clone().into(),
|
||||
));
|
||||
}
|
||||
current_group = option.group.clone();
|
||||
}
|
||||
entries.push(ConfigOptionPickerEntry::Option(option.clone()));
|
||||
}
|
||||
|
||||
entries
|
||||
}
|
||||
|
||||
async fn fuzzy_search_options(
|
||||
options: Vec<ConfigOptionValue>,
|
||||
query: &str,
|
||||
executor: BackgroundExecutor,
|
||||
) -> Vec<ConfigOptionValue> {
|
||||
let candidates = options
|
||||
.iter()
|
||||
.enumerate()
|
||||
.map(|(ix, opt)| StringMatchCandidate::new(ix, &opt.name))
|
||||
.collect::<Vec<_>>();
|
||||
|
||||
let mut matches = fuzzy::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| options[mat.candidate_id].clone())
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn find_option_name(
|
||||
options: &acp::SessionConfigSelectOptions,
|
||||
value_id: &acp::SessionConfigValueId,
|
||||
) -> Option<String> {
|
||||
match options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(opts) => opts
|
||||
.iter()
|
||||
.find(|o| &o.value == value_id)
|
||||
.map(|o| o.name.clone()),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => groups.iter().find_map(|group| {
|
||||
group
|
||||
.options
|
||||
.iter()
|
||||
.find(|o| &o.value == value_id)
|
||||
.map(|o| o.name.clone())
|
||||
}),
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
|
||||
fn count_config_options(option: &acp::SessionConfigOption) -> usize {
|
||||
match &option.kind {
|
||||
acp::SessionConfigKind::Select(select) => match &select.options {
|
||||
acp::SessionConfigSelectOptions::Ungrouped(options) => options.len(),
|
||||
acp::SessionConfigSelectOptions::Grouped(groups) => {
|
||||
groups.iter().map(|g| g.options.len()).sum()
|
||||
}
|
||||
_ => 0,
|
||||
},
|
||||
_ => 0,
|
||||
}
|
||||
}
|
||||
@@ -56,6 +56,7 @@ use workspace::{CollaboratorId, NewTerminal, Workspace};
|
||||
use zed_actions::agent::{Chat, ToggleModelSelector};
|
||||
use zed_actions::assistant::OpenRulesLibrary;
|
||||
|
||||
use super::config_options::ConfigOptionsView;
|
||||
use super::entry_view_state::EntryViewState;
|
||||
use crate::acp::AcpModelSelectorPopover;
|
||||
use crate::acp::ModeSelector;
|
||||
@@ -272,6 +273,7 @@ pub struct AcpThreadView {
|
||||
message_editor: Entity<MessageEditor>,
|
||||
focus_handle: FocusHandle,
|
||||
model_selector: Option<Entity<AcpModelSelectorPopover>>,
|
||||
config_options_view: Option<Entity<ConfigOptionsView>>,
|
||||
profile_selector: Option<Entity<ProfileSelector>>,
|
||||
notifications: Vec<WindowHandle<AgentNotification>>,
|
||||
notification_subscriptions: HashMap<WindowHandle<AgentNotification>, Vec<Subscription>>,
|
||||
@@ -430,6 +432,7 @@ impl AcpThreadView {
|
||||
login: None,
|
||||
message_editor,
|
||||
model_selector: None,
|
||||
config_options_view: None,
|
||||
profile_selector: None,
|
||||
notifications: Vec::new(),
|
||||
notification_subscriptions: HashMap::default(),
|
||||
@@ -614,42 +617,64 @@ impl AcpThreadView {
|
||||
|
||||
AgentDiff::set_active_thread(&workspace, thread.clone(), window, cx);
|
||||
|
||||
this.model_selector = thread
|
||||
// Check for config options first
|
||||
// Config options take precedence over legacy mode/model selectors
|
||||
// (feature flag gating happens at the data layer)
|
||||
let config_options_provider = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.model_selector(thread.read(cx).session_id())
|
||||
.map(|selector| {
|
||||
let agent_server = this.agent.clone();
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
cx.new(|cx| {
|
||||
AcpModelSelectorPopover::new(
|
||||
selector,
|
||||
agent_server,
|
||||
fs,
|
||||
PopoverMenuHandle::default(),
|
||||
this.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
.session_config_options(thread.read(cx).session_id(), cx);
|
||||
|
||||
let mode_selector = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.session_modes(thread.read(cx).session_id(), cx)
|
||||
.map(|session_modes| {
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
let focus_handle = this.focus_handle(cx);
|
||||
cx.new(|_cx| {
|
||||
ModeSelector::new(
|
||||
session_modes,
|
||||
this.agent.clone(),
|
||||
fs,
|
||||
focus_handle,
|
||||
)
|
||||
})
|
||||
});
|
||||
let mode_selector;
|
||||
if let Some(config_options) = config_options_provider {
|
||||
// Use config options - don't create mode_selector or model_selector
|
||||
let agent_server = this.agent.clone();
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
this.config_options_view = Some(cx.new(|cx| {
|
||||
ConfigOptionsView::new(config_options, agent_server, fs, window, cx)
|
||||
}));
|
||||
this.model_selector = None;
|
||||
mode_selector = None;
|
||||
} else {
|
||||
// Fall back to legacy mode/model selectors
|
||||
this.config_options_view = None;
|
||||
this.model_selector = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.model_selector(thread.read(cx).session_id())
|
||||
.map(|selector| {
|
||||
let agent_server = this.agent.clone();
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
cx.new(|cx| {
|
||||
AcpModelSelectorPopover::new(
|
||||
selector,
|
||||
agent_server,
|
||||
fs,
|
||||
PopoverMenuHandle::default(),
|
||||
this.focus_handle(cx),
|
||||
window,
|
||||
cx,
|
||||
)
|
||||
})
|
||||
});
|
||||
|
||||
mode_selector = thread
|
||||
.read(cx)
|
||||
.connection()
|
||||
.session_modes(thread.read(cx).session_id(), cx)
|
||||
.map(|session_modes| {
|
||||
let fs = this.project.read(cx).fs().clone();
|
||||
let focus_handle = this.focus_handle(cx);
|
||||
cx.new(|_cx| {
|
||||
ModeSelector::new(
|
||||
session_modes,
|
||||
this.agent.clone(),
|
||||
fs,
|
||||
focus_handle,
|
||||
)
|
||||
})
|
||||
});
|
||||
}
|
||||
|
||||
let mut subscriptions = vec![
|
||||
cx.subscribe_in(&thread, window, Self::handle_thread_event),
|
||||
@@ -1522,6 +1547,10 @@ impl AcpThreadView {
|
||||
// The connection keeps track of the mode
|
||||
cx.notify();
|
||||
}
|
||||
AcpThreadEvent::ConfigOptionsUpdated(_) => {
|
||||
// The watch task in ConfigOptionsView handles rebuilding selectors
|
||||
cx.notify();
|
||||
}
|
||||
}
|
||||
cx.notify();
|
||||
}
|
||||
@@ -4417,8 +4446,12 @@ impl AcpThreadView {
|
||||
.gap_1()
|
||||
.children(self.render_token_usage(cx))
|
||||
.children(self.profile_selector.clone())
|
||||
.children(self.mode_selector().cloned())
|
||||
.children(self.model_selector.clone())
|
||||
// Either config_options_view OR (mode_selector + model_selector)
|
||||
.children(self.config_options_view.clone())
|
||||
.when(self.config_options_view.is_none(), |this| {
|
||||
this.children(self.mode_selector().cloned())
|
||||
.children(self.model_selector.clone())
|
||||
})
|
||||
.child(self.render_send_button(cx)),
|
||||
),
|
||||
)
|
||||
|
||||
@@ -1371,6 +1371,8 @@ async fn open_new_agent_servers_entry_in_settings_editor(
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
},
|
||||
);
|
||||
}
|
||||
|
||||
@@ -1363,7 +1363,8 @@ impl AgentDiff {
|
||||
| AcpThreadEvent::PromptCapabilitiesUpdated
|
||||
| AcpThreadEvent::AvailableCommandsUpdated(_)
|
||||
| AcpThreadEvent::Retry(_)
|
||||
| AcpThreadEvent::ModeUpdated(_) => {}
|
||||
| AcpThreadEvent::ModeUpdated(_)
|
||||
| AcpThreadEvent::ConfigOptionsUpdated(_) => {}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -23,3 +23,9 @@ pub struct AgentV2FeatureFlag;
|
||||
impl FeatureFlag for AgentV2FeatureFlag {
|
||||
const NAME: &'static str = "agent-v2";
|
||||
}
|
||||
|
||||
pub struct AcpBetaFeatureFlag;
|
||||
|
||||
impl FeatureFlag for AcpBetaFeatureFlag {
|
||||
const NAME: &'static str = "acp-beta";
|
||||
}
|
||||
|
||||
@@ -1869,6 +1869,8 @@ pub struct BuiltinAgentServerSettings {
|
||||
pub default_mode: Option<String>,
|
||||
pub default_model: Option<String>,
|
||||
pub favorite_models: Vec<String>,
|
||||
pub default_config_options: HashMap<String, String>,
|
||||
pub favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
impl BuiltinAgentServerSettings {
|
||||
@@ -1893,6 +1895,8 @@ impl From<settings::BuiltinAgentServerSettings> for BuiltinAgentServerSettings {
|
||||
default_mode: value.default_mode,
|
||||
default_model: value.default_model,
|
||||
favorite_models: value.favorite_models,
|
||||
default_config_options: value.default_config_options,
|
||||
favorite_config_option_values: value.favorite_config_option_values,
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1928,6 +1932,18 @@ pub enum CustomAgentServerSettings {
|
||||
///
|
||||
/// Default: []
|
||||
favorite_models: Vec<String>,
|
||||
/// Default values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to value ID.
|
||||
///
|
||||
/// Default: {}
|
||||
default_config_options: HashMap<String, String>,
|
||||
/// Favorited values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to a list of favorited value IDs.
|
||||
///
|
||||
/// Default: {}
|
||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
},
|
||||
Extension {
|
||||
/// The default mode to use for this agent.
|
||||
@@ -1946,6 +1962,18 @@ pub enum CustomAgentServerSettings {
|
||||
///
|
||||
/// Default: []
|
||||
favorite_models: Vec<String>,
|
||||
/// Default values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to value ID.
|
||||
///
|
||||
/// Default: {}
|
||||
default_config_options: HashMap<String, String>,
|
||||
/// Favorited values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to a list of favorited value IDs.
|
||||
///
|
||||
/// Default: {}
|
||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
@@ -1983,6 +2011,34 @@ impl CustomAgentServerSettings {
|
||||
} => favorite_models,
|
||||
}
|
||||
}
|
||||
|
||||
pub fn default_config_option(&self, config_id: &str) -> Option<&str> {
|
||||
match self {
|
||||
CustomAgentServerSettings::Custom {
|
||||
default_config_options,
|
||||
..
|
||||
}
|
||||
| CustomAgentServerSettings::Extension {
|
||||
default_config_options,
|
||||
..
|
||||
} => default_config_options.get(config_id).map(|s| s.as_str()),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn favorite_config_option_values(&self, config_id: &str) -> Option<&[String]> {
|
||||
match self {
|
||||
CustomAgentServerSettings::Custom {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
}
|
||||
| CustomAgentServerSettings::Extension {
|
||||
favorite_config_option_values,
|
||||
..
|
||||
} => favorite_config_option_values
|
||||
.get(config_id)
|
||||
.map(|v| v.as_slice()),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||
@@ -1995,6 +2051,8 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||
default_mode,
|
||||
default_model,
|
||||
favorite_models,
|
||||
default_config_options,
|
||||
favorite_config_option_values,
|
||||
} => CustomAgentServerSettings::Custom {
|
||||
command: AgentServerCommand {
|
||||
path: PathBuf::from(shellexpand::tilde(&path.to_string_lossy()).as_ref()),
|
||||
@@ -2004,15 +2062,21 @@ impl From<settings::CustomAgentServerSettings> for CustomAgentServerSettings {
|
||||
default_mode,
|
||||
default_model,
|
||||
favorite_models,
|
||||
default_config_options,
|
||||
favorite_config_option_values,
|
||||
},
|
||||
settings::CustomAgentServerSettings::Extension {
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
favorite_models,
|
||||
favorite_config_option_values,
|
||||
} => CustomAgentServerSettings::Extension {
|
||||
default_mode,
|
||||
default_model,
|
||||
default_config_options,
|
||||
favorite_models,
|
||||
favorite_config_option_values,
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -2339,6 +2403,8 @@ mod extension_agent_tests {
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
};
|
||||
|
||||
let BuiltinAgentServerSettings { path, .. } = settings.into();
|
||||
@@ -2356,6 +2422,8 @@ mod extension_agent_tests {
|
||||
default_mode: None,
|
||||
default_model: None,
|
||||
favorite_models: vec![],
|
||||
default_config_options: Default::default(),
|
||||
favorite_config_option_values: Default::default(),
|
||||
};
|
||||
|
||||
let converted: CustomAgentServerSettings = settings.into();
|
||||
|
||||
@@ -370,6 +370,20 @@ pub struct BuiltinAgentServerSettings {
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
pub favorite_models: Vec<String>,
|
||||
/// Default values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to value ID.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(default)]
|
||||
pub default_config_options: HashMap<String, String>,
|
||||
/// Favorited values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to a list of favorited value IDs.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(default)]
|
||||
pub favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
}
|
||||
|
||||
#[with_fallible_options]
|
||||
@@ -401,6 +415,20 @@ pub enum CustomAgentServerSettings {
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
favorite_models: Vec<String>,
|
||||
/// Default values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to value ID.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(default)]
|
||||
default_config_options: HashMap<String, String>,
|
||||
/// Favorited values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to a list of favorited value IDs.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(default)]
|
||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
},
|
||||
Extension {
|
||||
/// The default mode to use for this agent.
|
||||
@@ -422,5 +450,19 @@ pub enum CustomAgentServerSettings {
|
||||
/// Default: []
|
||||
#[serde(default)]
|
||||
favorite_models: Vec<String>,
|
||||
/// Default values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to value ID.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(default)]
|
||||
default_config_options: HashMap<String, String>,
|
||||
/// Favorited values for session config options.
|
||||
///
|
||||
/// This is a map from config option ID to a list of favorited value IDs.
|
||||
///
|
||||
/// Default: {}
|
||||
#[serde(default)]
|
||||
favorite_config_option_values: HashMap<String, Vec<String>>,
|
||||
},
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user