acp: Support session modes (e.g. CC plan mode) (#37632)

Adds support for [ACP session
modes](https://github.com/zed-industries/agent-client-protocol/pull/67)
enabling plan and other permission modes in CC:


https://github.com/user-attachments/assets/dea18d82-4da6-465e-983b-02b77c6dcf15


Release Notes:

- Claude Code: Add support for plan mode, and all other permission modes

---------

Co-authored-by: Bennet Bo Fenner <bennetbo@gmx.de>
Co-authored-by: Richard Feldman <oss@rtfeldman.com>
Co-authored-by: Danilo Leal <daniloleal09@gmail.com>
This commit is contained in:
Agus Zubiaga 2025-09-09 13:28:02 -03:00 committed by GitHub
parent ad02f6b9e3
commit 5e397e85b1
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
26 changed files with 886 additions and 143 deletions

4
Cargo.lock generated
View file

@ -196,9 +196,9 @@ dependencies = [
[[package]]
name = "agent-client-protocol"
version = "0.2.0-alpha.6"
version = "0.2.0-alpha.8"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d02292efd75080932b6466471d428c70e2ac06908ae24792fc7c36ecbaf67ca"
checksum = "08539e8d6b2ccca6cd00afdd42211698f7677adef09108a09414c11f1f45fdaf"
dependencies = [
"anyhow",
"async-broadcast",

View file

@ -433,7 +433,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
agent-client-protocol = { version = "0.2.0-alpha.6", features = ["unstable"]}
agent-client-protocol = { version = "0.2.0-alpha.8", features = ["unstable"] }
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"

View file

@ -328,6 +328,12 @@
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
@ -335,7 +341,7 @@
"enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
"ctrl-shift-n": "agent::RejectAll",
}
},
{
@ -345,7 +351,8 @@
"ctrl-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{

View file

@ -378,6 +378,12 @@
"ctrl--": "pane::GoBack"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
"cmd-enter": "menu::Confirm"
}
},
{
"context": "AcpThread > Editor && !use_modifier_to_send",
"use_key_equivalents": true,
@ -385,7 +391,8 @@
"enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{
@ -395,7 +402,8 @@
"cmd-enter": "agent::Chat",
"shift-ctrl-r": "agent::OpenAgentDiff",
"cmd-shift-y": "agent::KeepAll",
"cmd-shift-n": "agent::RejectAll"
"cmd-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{

View file

@ -336,6 +336,12 @@
"enter": "agent::AcceptSuggestedContext"
}
},
{
"context": "AcpThread > ModeSelector",
"bindings": {
"ctrl-enter": "menu::Confirm"
}
},
{
"context": "AcpThread > Editor",
"use_key_equivalents": true,
@ -343,7 +349,8 @@
"enter": "agent::Chat",
"ctrl-shift-r": "agent::OpenAgentDiff",
"ctrl-shift-y": "agent::KeepAll",
"ctrl-shift-n": "agent::RejectAll"
"ctrl-shift-n": "agent::RejectAll",
"shift-tab": "agent::CycleModeSelector"
}
},
{

View file

@ -828,6 +828,9 @@
// }
],
// When enabled, the agent can run potentially destructive actions without asking for your confirmation.
//
// Note: This setting has no effect on external agents that support permission modes, such as Claude Code.
// You can set `agent_servers.claude.default_mode` to `bypassPermissions` to skip all permission requests.
"always_allow_tool_actions": false,
// When enabled, the agent will stream edits.
"stream_edits": false,

View file

@ -18,8 +18,8 @@ test-support = ["gpui/test-support", "project/test-support", "dep:parking_lot"]
[dependencies]
action_log.workspace = true
agent-client-protocol.workspace = true
anyhow.workspace = true
agent_settings.workspace = true
anyhow.workspace = true
buffer_diff.workspace = true
collections.workspace = true
editor.workspace = true

View file

@ -805,6 +805,7 @@ pub enum AcpThreadEvent {
PromptCapabilitiesUpdated,
Refusal,
AvailableCommandsUpdated(Vec<acp::AvailableCommand>),
ModeUpdated(acp::SessionModeId),
}
impl EventEmitter<AcpThreadEvent> for AcpThread {}
@ -1007,6 +1008,9 @@ impl AcpThread {
acp::SessionUpdate::AvailableCommandsUpdate { available_commands } => {
cx.emit(AcpThreadEvent::AvailableCommandsUpdated(available_commands))
}
acp::SessionUpdate::CurrentModeUpdate { current_mode_id } => {
cx.emit(AcpThreadEvent::ModeUpdated(current_mode_id))
}
}
Ok(())
}
@ -1303,11 +1307,12 @@ impl AcpThread {
&mut self,
tool_call: acp::ToolCallUpdate,
options: Vec<acp::PermissionOption>,
respect_always_allow_setting: bool,
cx: &mut Context<Self>,
) -> Result<BoxFuture<'static, acp::RequestPermissionOutcome>> {
let (tx, rx) = oneshot::channel();
if AgentSettings::get_global(cx).always_allow_tool_actions {
if respect_always_allow_setting && AgentSettings::get_global(cx).always_allow_tool_actions {
// Don't use AllowAlways, because then if you were to turn off always_allow_tool_actions,
// some tools would (incorrectly) continue to auto-accept.
if let Some(allow_once_option) = options.iter().find_map(|option| {

View file

@ -75,6 +75,15 @@ pub trait AgentConnection {
fn telemetry(&self) -> Option<Rc<dyn AgentTelemetry>> {
None
}
fn session_modes(
&self,
_session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn AgentSessionModes>> {
None
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any>;
}
@ -109,6 +118,14 @@ pub trait AgentTelemetry {
) -> Task<Result<serde_json::Value>>;
}
pub trait AgentSessionModes {
fn current_mode(&self) -> acp::SessionModeId;
fn all_modes(&self) -> Vec<acp::SessionMode>;
fn set_mode(&self, mode: acp::SessionModeId, cx: &mut App) -> Task<Result<()>>;
}
#[derive(Debug)]
pub struct AuthRequired {
pub description: Option<String>,
@ -397,6 +414,7 @@ mod test_support {
thread.request_tool_call_authorization(
tool_call.clone().into(),
options.clone(),
false,
cx,
)
})??

View file

@ -771,7 +771,9 @@ impl NativeAgentConnection {
response,
}) => {
let outcome_task = acp_thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(tool_call, options, cx)
thread.request_tool_call_authorization(
tool_call, options, true, cx,
)
})??;
cx.background_spawn(async move {
if let acp::RequestPermissionOutcome::Selected { option_id } =

View file

@ -9,6 +9,7 @@ use futures::io::BufReader;
use project::Project;
use project::agent_server_store::AgentServerCommand;
use serde::Deserialize;
use util::ResultExt;
use std::path::PathBuf;
use std::{any::Any, cell::RefCell};
@ -30,6 +31,7 @@ pub struct AcpConnection {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
auth_methods: Vec<acp::AuthMethod>,
agent_capabilities: acp::AgentCapabilities,
default_mode: Option<acp::SessionModeId>,
root_dir: PathBuf,
_io_task: Task<Result<()>>,
_wait_task: Task<Result<()>>,
@ -39,16 +41,26 @@ pub struct AcpConnection {
pub struct AcpSession {
thread: WeakEntity<AcpThread>,
suppress_abort_err: bool,
session_modes: Option<Rc<RefCell<acp::SessionModeState>>>,
}
pub async fn connect(
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, is_remote, cx).await?;
let conn = AcpConnection::stdio(
server_name,
command.clone(),
root_dir,
default_mode,
is_remote,
cx,
)
.await?;
Ok(Rc::new(conn) as _)
}
@ -59,6 +71,7 @@ impl AcpConnection {
server_name: SharedString,
command: AgentServerCommand,
root_dir: &Path,
default_mode: Option<acp::SessionModeId>,
is_remote: bool,
cx: &mut AsyncApp,
) -> Result<Self> {
@ -157,6 +170,7 @@ impl AcpConnection {
server_name,
sessions,
agent_capabilities: response.agent_capabilities,
default_mode,
_io_task: io_task,
_wait_task: wait_task,
_stderr_task: stderr_task,
@ -179,8 +193,10 @@ impl AgentConnection for AcpConnection {
cwd: &Path,
cx: &mut App,
) -> Task<Result<Entity<AcpThread>>> {
let name = self.server_name.clone();
let conn = self.connection.clone();
let sessions = self.sessions.clone();
let default_mode = self.default_mode.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() {
@ -190,7 +206,7 @@ impl AgentConnection for AcpConnection {
.filter_map(|id| {
let configuration = context_server_store.configuration_for_server(id)?;
let command = configuration.command();
Some(acp::McpServer {
Some(acp::McpServer::Stdio {
name: id.0.to_string(),
command: command.path.clone(),
args: command.args.clone(),
@ -232,6 +248,53 @@ impl AgentConnection for AcpConnection {
}
})?;
let modes = response.modes.map(|modes| Rc::new(RefCell::new(modes)));
if let Some(default_mode) = default_mode {
if let Some(modes) = modes.as_ref() {
let mut modes_ref = modes.borrow_mut();
let has_mode = modes_ref.available_modes.iter().any(|mode| mode.id == default_mode);
if has_mode {
let initial_mode_id = modes_ref.current_mode_id.clone();
cx.spawn({
let default_mode = default_mode.clone();
let session_id = response.session_id.clone();
let modes = modes.clone();
async move |_| {
let result = conn.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id: default_mode,
})
.await.log_err();
if result.is_none() {
modes.borrow_mut().current_mode_id = initial_mode_id;
}
}
}).detach();
modes_ref.current_mode_id = default_mode;
} else {
let available_modes = modes_ref
.available_modes
.iter()
.map(|mode| format!("- `{}`: {}", mode.id, mode.name))
.collect::<Vec<_>>()
.join("\n");
log::warn!(
"`{default_mode}` is not valid {name} mode. Available options:\n{available_modes}",
);
}
} else {
log::warn!(
"`{name}` does not support modes, but `default_mode` was set in settings.",
);
}
}
let session_id = response.session_id;
let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
let thread = cx.new(|cx| {
@ -250,6 +313,7 @@ impl AgentConnection for AcpConnection {
let session = AcpSession {
thread: thread.downgrade(),
suppress_abort_err: false,
session_modes: modes
};
sessions.borrow_mut().insert(session_id, session);
@ -346,11 +410,77 @@ impl AgentConnection for AcpConnection {
.detach();
}
fn session_modes(
&self,
session_id: &acp::SessionId,
_cx: &App,
) -> Option<Rc<dyn acp_thread::AgentSessionModes>> {
let sessions = self.sessions.clone();
let sessions_ref = sessions.borrow();
let Some(session) = sessions_ref.get(session_id) else {
return None;
};
if let Some(modes) = session.session_modes.as_ref() {
Some(Rc::new(AcpSessionModes {
connection: self.connection.clone(),
session_id: session_id.clone(),
state: modes.clone(),
}) as _)
} else {
None
}
}
fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
self
}
}
struct AcpSessionModes {
session_id: acp::SessionId,
connection: Rc<acp::ClientSideConnection>,
state: Rc<RefCell<acp::SessionModeState>>,
}
impl acp_thread::AgentSessionModes for AcpSessionModes {
fn current_mode(&self) -> acp::SessionModeId {
self.state.borrow().current_mode_id.clone()
}
fn all_modes(&self) -> Vec<acp::SessionMode> {
self.state.borrow().available_modes.clone()
}
fn set_mode(&self, mode_id: acp::SessionModeId, cx: &mut App) -> Task<Result<()>> {
let connection = self.connection.clone();
let session_id = self.session_id.clone();
let old_mode_id;
{
let mut state = self.state.borrow_mut();
old_mode_id = state.current_mode_id.clone();
state.current_mode_id = mode_id.clone();
};
let state = self.state.clone();
cx.foreground_executor().spawn(async move {
let result = connection
.set_session_mode(acp::SetSessionModeRequest {
session_id,
mode_id,
})
.await;
if result.is_err() {
state.borrow_mut().current_mode_id = old_mode_id;
}
result?;
Ok(())
})
}
}
struct ClientDelegate {
sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
cx: AsyncApp,
@ -361,13 +491,27 @@ impl acp::Client for ClientDelegate {
&self,
arguments: acp::RequestPermissionRequest,
) -> Result<acp::RequestPermissionResponse, acp::Error> {
let respect_always_allow_setting;
let thread;
{
let sessions_ref = self.sessions.borrow();
let session = sessions_ref
.get(&arguments.session_id)
.context("Failed to get session")?;
respect_always_allow_setting = session.session_modes.is_none();
thread = session.thread.clone();
}
let cx = &mut self.cx.clone();
let task = self
.session_thread(&arguments.session_id)?
.update(cx, |thread, cx| {
thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
})??;
let task = thread.update(cx, |thread, cx| {
thread.request_tool_call_authorization(
arguments.tool_call,
arguments.options,
respect_always_allow_setting,
cx,
)
})??;
let outcome = task.await;
@ -410,10 +554,24 @@ impl acp::Client for ClientDelegate {
&self,
notification: acp::SessionNotification,
) -> Result<(), acp::Error> {
self.session_thread(&notification.session_id)?
.update(&mut self.cx.clone(), |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
let sessions = self.sessions.borrow();
let session = sessions
.get(&notification.session_id)
.context("Failed to get session")?;
if let acp::SessionUpdate::CurrentModeUpdate { current_mode_id } = &notification.update {
if let Some(session_modes) = &session.session_modes {
session_modes.borrow_mut().current_mode_id = current_mode_id.clone();
} else {
log::error!(
"Got a `CurrentModeUpdate` notification, but they agent didn't specify `modes` during setting setup."
);
}
}
session.thread.update(&mut self.cx.clone(), |thread, cx| {
thread.handle_session_update(notification.update, cx)
})??;
Ok(())
}

View file

@ -8,6 +8,7 @@ pub mod e2e_tests;
pub use claude::*;
pub use custom::*;
use fs::Fs;
pub use gemini::*;
use project::agent_server_store::AgentServerStore;
@ -15,7 +16,7 @@ use acp_thread::AgentConnection;
use anyhow::Result;
use gpui::{App, Entity, SharedString, Task};
use project::Project;
use std::{any::Any, path::Path, rc::Rc};
use std::{any::Any, path::Path, rc::Rc, sync::Arc};
pub use acp::AcpConnection;
@ -50,6 +51,16 @@ pub trait AgentServer: Send {
fn logo(&self) -> ui::IconName;
fn name(&self) -> SharedString;
fn telemetry_id(&self) -> &'static str;
fn default_mode(&self, _cx: &mut App) -> Option<agent_client_protocol::SessionModeId> {
None
}
fn set_default_mode(
&self,
_mode_id: Option<agent_client_protocol::SessionModeId>,
_fs: Arc<dyn Fs>,
_cx: &mut App,
) {
}
fn connect(
&self,

View file

@ -1,10 +1,14 @@
use agent_client_protocol as acp;
use fs::Fs;
use settings::{SettingsStore, update_settings_file};
use std::path::Path;
use std::rc::Rc;
use std::sync::Arc;
use std::{any::Any, path::PathBuf};
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::CLAUDE_CODE_NAME;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, CLAUDE_CODE_NAME};
use crate::{AgentServer, AgentServerDelegate};
use acp_thread::AgentConnection;
@ -30,6 +34,22 @@ impl AgentServer for ClaudeCode {
ui::IconName::AiClaude
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<AllAgentServersSettings>(None).claude.clone()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
update_settings_file::<AllAgentServersSettings>(fs, cx, |settings, _| {
settings.claude.get_or_insert_default().default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@ -40,6 +60,7 @@ impl AgentServer for ClaudeCode {
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let store = delegate.store.downgrade();
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
let (command, root_dir, login) = store
@ -56,8 +77,15 @@ impl AgentServer for ClaudeCode {
))
})??
.await?;
let connection =
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
let connection = crate::acp::connect(
name,
command,
root_dir.as_ref(),
default_mode,
is_remote,
cx,
)
.await?;
Ok((connection, login))
})
}

View file

@ -1,9 +1,12 @@
use crate::AgentServerDelegate;
use acp_thread::AgentConnection;
use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use gpui::{App, SharedString, Task};
use project::agent_server_store::ExternalAgentServerName;
use std::{path::Path, rc::Rc};
use fs::Fs;
use gpui::{App, AppContext as _, SharedString, Task};
use project::agent_server_store::{AllAgentServersSettings, ExternalAgentServerName};
use settings::{SettingsStore, update_settings_file};
use std::{path::Path, rc::Rc, sync::Arc};
use ui::IconName;
/// A generic agent server implementation for custom user-defined agents
@ -30,6 +33,27 @@ impl crate::AgentServer for CustomAgentServer {
IconName::Terminal
}
fn default_mode(&self, cx: &mut App) -> Option<acp::SessionModeId> {
let settings = cx.read_global(|settings: &SettingsStore, _| {
settings
.get::<AllAgentServersSettings>(None)
.custom
.get(&self.name())
.cloned()
});
settings
.as_ref()
.and_then(|s| s.default_mode.clone().map(|m| acp::SessionModeId(m.into())))
}
fn set_default_mode(&self, mode_id: Option<acp::SessionModeId>, fs: Arc<dyn Fs>, cx: &mut App) {
let name = self.name();
update_settings_file::<AllAgentServersSettings>(fs, cx, move |settings, _| {
settings.custom.get_mut(&name).unwrap().default_mode = mode_id.map(|m| m.to_string())
});
}
fn connect(
&self,
root_dir: Option<&Path>,
@ -39,6 +63,7 @@ impl crate::AgentServer for CustomAgentServer {
let name = self.name();
let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().to_string());
let is_remote = delegate.project.read(cx).is_via_remote_server();
let default_mode = self.default_mode(cx);
let store = delegate.store.downgrade();
cx.spawn(async move |cx| {
@ -58,8 +83,15 @@ impl crate::AgentServer for CustomAgentServer {
))
})??
.await?;
let connection =
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
let connection = crate::acp::connect(
name,
command,
root_dir.as_ref(),
default_mode,
is_remote,
cx,
)
.await?;
Ok((connection, login))
})
}

View file

@ -5,7 +5,7 @@ use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{AppContext, Entity, TestAppContext};
use indoc::indoc;
#[cfg(test)]
use project::agent_server_store::{AgentServerCommand, CustomAgentServerSettings};
use project::agent_server_store::BuiltinAgentServerSettings;
use project::{FakeFs, Project, agent_server_store::AllAgentServersSettings};
use std::{
path::{Path, PathBuf},
@ -472,12 +472,12 @@ pub async fn init_test(cx: &mut TestAppContext) -> Arc<FakeFs> {
#[cfg(test)]
AllAgentServersSettings::override_global(
AllAgentServersSettings {
claude: Some(CustomAgentServerSettings {
command: AgentServerCommand {
path: "claude-code-acp".into(),
args: vec![],
env: None,
},
claude: Some(BuiltinAgentServerSettings {
path: Some("claude-code-acp".into()),
args: None,
env: None,
ignore_system_version: None,
default_mode: None,
}),
gemini: Some(crate::gemini::tests::local_command().into()),
custom: collections::HashMap::default(),

View file

@ -40,6 +40,7 @@ impl AgentServer for Gemini {
let proxy_url = cx.read_global(|settings: &SettingsStore, _| {
settings.get::<ProxySettings>(None).proxy.clone()
});
let default_mode = self.default_mode(cx);
cx.spawn(async move |cx| {
let mut extra_env = HashMap::default();
@ -69,8 +70,15 @@ impl AgentServer for Gemini {
command.args.push(proxy_url_value.clone());
}
let connection =
crate::acp::connect(name, command, root_dir.as_ref(), is_remote, cx).await?;
let connection = crate::acp::connect(
name,
command,
root_dir.as_ref(),
default_mode,
is_remote,
cx,
)
.await?;
Ok((connection, login))
})
}

View file

@ -0,0 +1,125 @@
use agent_client_protocol as acp;
use std::path::PathBuf;
use crate::AgentServerCommand;
use anyhow::Result;
use collections::HashMap;
use gpui::{App, SharedString};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsKey, SettingsSources, SettingsUi};
pub fn init(cx: &mut App) {
AllAgentServersSettings::register(cx);
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, SettingsUi, SettingsKey)]
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<BuiltinAgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
pub custom: HashMap<SharedString, CustomAgentServerSettings>,
}
#[derive(Default, Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct BuiltinAgentServerSettings {
/// Absolute path to a binary to be used when launching this agent.
///
/// This can be used to run a specific binary without automatic downloads or searching `$PATH`.
#[serde(rename = "command")]
pub path: Option<PathBuf>,
/// If a binary is specified in `command`, it will be passed these arguments.
pub args: Option<Vec<String>>,
/// If a binary is specified in `command`, it will be passed these environment variables.
pub env: Option<HashMap<String, String>>,
/// Whether to skip searching `$PATH` for an agent server binary when
/// launching this agent.
///
/// This has no effect if a `command` is specified. Otherwise, when this is
/// `false`, Zed will search `$PATH` for an agent server binary and, if one
/// is found, use it for threads with this agent. If no agent binary is
/// found on `$PATH`, Zed will automatically install and use its own binary.
/// When this is `true`, Zed will not search `$PATH`, and will always use
/// its own binary.
///
/// Default: true
pub ignore_system_version: Option<bool>,
/// The default mode for new threads.
///
/// Note: Not all agents support modes.
///
/// Default: None
#[serde(skip_serializing_if = "Option::is_none")]
pub default_mode: Option<acp::SessionModeId>,
}
impl BuiltinAgentServerSettings {
pub(crate) fn custom_command(self) -> Option<AgentServerCommand> {
self.path.map(|path| AgentServerCommand {
path,
args: self.args.unwrap_or_default(),
env: self.env,
})
}
}
impl From<AgentServerCommand> for BuiltinAgentServerSettings {
fn from(value: AgentServerCommand) -> Self {
BuiltinAgentServerSettings {
path: Some(value.path),
args: Some(value.args),
env: value.env,
..Default::default()
}
}
}
#[derive(Deserialize, Serialize, Clone, JsonSchema, Debug, PartialEq)]
pub struct CustomAgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
/// The default mode for new threads.
///
/// Note: Not all agents support modes.
///
/// Default: None
#[serde(skip_serializing_if = "Option::is_none")]
pub default_mode: Option<acp::SessionModeId>,
}
impl settings::Settings for AllAgentServersSettings {
type FileContent = Self;
fn load(sources: SettingsSources<Self::FileContent>, _: &mut App) -> Result<Self> {
let mut settings = AllAgentServersSettings::default();
for AllAgentServersSettings {
gemini,
claude,
custom,
} in sources.defaults_and_customizations()
{
if gemini.is_some() {
settings.gemini = gemini.clone();
}
if claude.is_some() {
settings.claude = claude.clone();
}
// Merge custom agents
for (name, config) in custom {
// Skip built-in agent names to avoid conflicts
if name != "gemini" && name != "claude" {
settings.custom.insert(name.clone(), config.clone());
}
}
}
Ok(settings)
}
fn import_from_vscode(_vscode: &settings::VsCodeSettings, _current: &mut Self::FileContent) {}
}

View file

@ -269,6 +269,10 @@ pub struct AgentSettingsContent {
/// Whenever a tool action would normally wait for your confirmation
/// that you allow it, always choose to allow it.
///
/// This setting has no effect on external agents that support permission modes, such as Claude Code.
///
/// Set `agent_servers.claude.default_mode` to `bypassPermissions`, to disable all permission requests when using Claude Code.
///
/// Default: false
always_allow_tool_actions: Option<bool>,
/// Where to show a popup notification when the agent is waiting for user input.

View file

@ -1,11 +1,13 @@
mod completion_provider;
mod entry_view_state;
mod message_editor;
mod mode_selector;
mod model_selector;
mod model_selector_popover;
mod thread_history;
mod thread_view;
pub use mode_selector::ModeSelector;
pub use model_selector::AcpModelSelector;
pub use model_selector_popover::AcpModelSelectorPopover;
pub use thread_history::*;

View file

@ -0,0 +1,230 @@
use acp_thread::AgentSessionModes;
use agent_client_protocol as acp;
use agent_servers::AgentServer;
use fs::Fs;
use gpui::{Context, Entity, FocusHandle, WeakEntity, Window, prelude::*};
use std::{rc::Rc, sync::Arc};
use ui::{
Button, ContextMenu, ContextMenuEntry, KeyBinding, PopoverMenu, PopoverMenuHandle, Tooltip,
prelude::*,
};
use crate::{CycleModeSelector, ToggleProfileSelector};
pub struct ModeSelector {
connection: Rc<dyn AgentSessionModes>,
agent_server: Rc<dyn AgentServer>,
menu_handle: PopoverMenuHandle<ContextMenu>,
focus_handle: FocusHandle,
fs: Arc<dyn Fs>,
setting_mode: bool,
}
impl ModeSelector {
pub fn new(
session_modes: Rc<dyn AgentSessionModes>,
agent_server: Rc<dyn AgentServer>,
fs: Arc<dyn Fs>,
focus_handle: FocusHandle,
) -> Self {
Self {
connection: session_modes,
agent_server,
menu_handle: PopoverMenuHandle::default(),
fs,
setting_mode: false,
focus_handle,
}
}
pub fn menu_handle(&self) -> PopoverMenuHandle<ContextMenu> {
self.menu_handle.clone()
}
pub fn cycle_mode(&mut self, _window: &mut Window, cx: &mut Context<Self>) {
let all_modes = self.connection.all_modes();
let current_mode = self.connection.current_mode();
let current_index = all_modes
.iter()
.position(|mode| mode.id.0 == current_mode.0)
.unwrap_or(0);
let next_index = (current_index + 1) % all_modes.len();
self.set_mode(all_modes[next_index].id.clone(), cx);
}
pub fn set_mode(&mut self, mode: acp::SessionModeId, cx: &mut Context<Self>) {
let task = self.connection.set_mode(mode, cx);
self.setting_mode = true;
cx.notify();
cx.spawn(async move |this: WeakEntity<ModeSelector>, cx| {
if let Err(err) = task.await {
log::error!("Failed to set session mode: {:?}", err);
}
this.update(cx, |this, cx| {
this.setting_mode = false;
cx.notify();
})
.ok();
})
.detach();
}
fn build_context_menu(
&self,
window: &mut Window,
cx: &mut Context<Self>,
) -> Entity<ContextMenu> {
let weak_self = cx.weak_entity();
ContextMenu::build(window, cx, move |mut menu, _window, cx| {
let all_modes = self.connection.all_modes();
let current_mode = self.connection.current_mode();
let default_mode = self.agent_server.default_mode(cx);
for mode in all_modes {
let is_selected = &mode.id == &current_mode;
let is_default = Some(&mode.id) == default_mode.as_ref();
let entry = ContextMenuEntry::new(mode.name.clone())
.toggleable(IconPosition::End, is_selected);
let entry = if let Some(description) = &mode.description {
entry.documentation_aside(ui::DocumentationSide::Left, {
let description = description.clone();
move |cx| {
v_flex()
.gap_1()
.child(Label::new(description.clone()))
.child(
h_flex()
.pt_1()
.border_t_1()
.border_color(cx.theme().colors().border_variant)
.gap_0p5()
.text_sm()
.text_color(Color::Muted.color(cx))
.child("Hold")
.child(div().pt_0p5().children(ui::render_modifiers(
&gpui::Modifiers::secondary_key(),
PlatformStyle::platform(),
None,
Some(ui::TextSize::Default.rems(cx).into()),
true,
)))
.child(div().map(|this| {
if is_default {
this.child("to also unset as default")
} else {
this.child("to also set as default")
}
})),
)
.into_any_element()
}
})
} else {
entry
};
menu.push_item(entry.handler({
let mode_id = mode.id.clone();
let weak_self = weak_self.clone();
move |window, cx| {
weak_self
.update(cx, |this, cx| {
if window.modifiers().secondary() {
this.agent_server.set_default_mode(
if is_default {
None
} else {
Some(mode_id.clone())
},
this.fs.clone(),
cx,
);
}
this.set_mode(mode_id.clone(), cx);
})
.ok();
}
}));
}
menu.key_context("ModeSelector")
})
}
}
impl Render for ModeSelector {
fn render(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
let current_mode_id = self.connection.current_mode();
let current_mode_name = self
.connection
.all_modes()
.iter()
.find(|mode| mode.id == current_mode_id)
.map(|mode| mode.name.clone())
.unwrap_or_else(|| "Unknown".into());
let this = cx.entity();
let trigger_button = Button::new("mode-selector-trigger", current_mode_name)
.label_size(LabelSize::Small)
.style(ButtonStyle::Subtle)
.color(Color::Muted)
.icon(IconName::ChevronDown)
.icon_size(IconSize::XSmall)
.icon_position(IconPosition::End)
.icon_color(Color::Muted)
.disabled(self.setting_mode);
PopoverMenu::new("mode-selector")
.trigger_with_tooltip(
trigger_button,
Tooltip::element({
let focus_handle = self.focus_handle.clone();
move |window, cx| {
v_flex()
.gap_1()
.child(
h_flex()
.pb_1()
.gap_2()
.justify_between()
.border_b_1()
.border_color(cx.theme().colors().border_variant)
.child(Label::new("Cycle Through Modes"))
.children(KeyBinding::for_action_in(
&CycleModeSelector,
&focus_handle,
window,
cx,
)),
)
.child(
h_flex()
.gap_2()
.justify_between()
.child(Label::new("Toggle Mode Menu"))
.children(KeyBinding::for_action_in(
&ToggleProfileSelector,
&focus_handle,
window,
cx,
)),
)
.into_any()
}
}),
)
.anchor(gpui::Corner::BottomRight)
.with_handle(self.menu_handle.clone())
.menu(move |window, cx| {
Some(this.update(cx, |this, cx| this.build_context_menu(window, cx)))
})
}
}

View file

@ -54,6 +54,7 @@ use zed_actions::assistant::OpenRulesLibrary;
use super::entry_view_state::EntryViewState;
use crate::acp::AcpModelSelectorPopover;
use crate::acp::ModeSelector;
use crate::acp::entry_view_state::{EntryViewEvent, ViewEvent};
use crate::acp::message_editor::{MessageEditor, MessageEditorEvent};
use crate::agent_diff::AgentDiff;
@ -64,8 +65,9 @@ use crate::ui::{
AgentNotification, AgentNotificationEvent, BurnModeTooltip, UnavailableEditingTooltip,
};
use crate::{
AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, ExpandMessageEditor, Follow,
KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode, ToggleProfileSelector,
AgentDiffPane, AgentPanel, ContinueThread, ContinueWithBurnMode, CycleModeSelector,
ExpandMessageEditor, Follow, KeepAll, OpenAgentDiff, OpenHistory, RejectAll, ToggleBurnMode,
ToggleProfileSelector,
};
pub const MIN_EDITOR_LINES: usize = 4;
@ -298,6 +300,7 @@ enum ThreadState {
Ready {
thread: Entity<AcpThread>,
title_editor: Option<Entity<Editor>>,
mode_selector: Option<Entity<ModeSelector>>,
_subscriptions: Vec<Subscription>,
},
LoadError(LoadError),
@ -396,6 +399,7 @@ impl AcpThreadView {
message_editor,
model_selector: None,
profile_selector: None,
notifications: Vec::new(),
notification_subscriptions: HashMap::default(),
list_state: list_state.clone(),
@ -594,6 +598,23 @@ impl AcpThreadView {
})
});
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 mut subscriptions = vec![
cx.subscribe_in(&thread, window, Self::handle_thread_event),
cx.observe(&action_log, |_, _, cx| cx.notify()),
@ -615,9 +636,11 @@ impl AcpThreadView {
} else {
None
};
this.thread_state = ThreadState::Ready {
thread,
title_editor,
mode_selector,
_subscriptions: subscriptions,
};
this.message_editor.focus_handle(cx).focus(window);
@ -770,6 +793,15 @@ impl AcpThreadView {
}
}
pub fn mode_selector(&self) -> Option<&Entity<ModeSelector>> {
match &self.thread_state {
ThreadState::Ready { mode_selector, .. } => mode_selector.as_ref(),
ThreadState::Unauthenticated { .. }
| ThreadState::Loading { .. }
| ThreadState::LoadError { .. } => None,
}
}
pub fn title(&self, cx: &App) -> SharedString {
match &self.thread_state {
ThreadState::Ready { .. } | ThreadState::Unauthenticated { .. } => "New Thread".into(),
@ -1365,6 +1397,10 @@ impl AcpThreadView {
self.available_commands.replace(available_commands);
}
AcpThreadEvent::ModeUpdated(_mode) => {
// The connection keeps track of the mode
cx.notify();
}
}
cx.notify();
}
@ -2055,6 +2091,7 @@ impl AcpThreadView {
acp::ToolKind::Execute => IconName::ToolTerminal,
acp::ToolKind::Think => IconName::ToolThink,
acp::ToolKind::Fetch => IconName::ToolWeb,
acp::ToolKind::SwitchMode => IconName::ArrowRightLeft,
acp::ToolKind::Other => IconName::ToolHammer,
})
}
@ -2105,59 +2142,67 @@ impl AcpThreadView {
})
};
let tool_output_display = if is_open {
match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
.w_full()
.children(tool_call.content.iter().map(|content| {
div()
.child(self.render_tool_call_content(
entry_ix,
content,
tool_call,
use_card_layout,
window,
cx,
))
.into_any_element()
}))
.child(self.render_permission_buttons(
options,
entry_ix,
tool_call.id.clone(),
cx,
))
.into_any(),
ToolCallStatus::Pending | ToolCallStatus::InProgress
if is_edit
&& tool_call.content.is_empty()
&& self.as_native_connection(cx).is_some() =>
{
self.render_diff_loading(cx).into_any()
}
ToolCallStatus::Pending
| ToolCallStatus::InProgress
| ToolCallStatus::Completed
| ToolCallStatus::Failed
| ToolCallStatus::Canceled => v_flex()
.w_full()
.children(tool_call.content.iter().map(|content| {
div().child(self.render_tool_call_content(
let tool_output_display =
if is_open {
match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { options, .. } => v_flex()
.w_full()
.children(tool_call.content.iter().enumerate().map(
|(content_ix, content)| {
div()
.child(self.render_tool_call_content(
entry_ix,
content,
content_ix,
tool_call,
use_card_layout,
window,
cx,
))
.into_any_element()
},
))
.child(self.render_permission_buttons(
tool_call.kind,
options,
entry_ix,
content,
tool_call,
use_card_layout,
window,
tool_call.id.clone(),
cx,
))
}))
.into_any(),
ToolCallStatus::Rejected => Empty.into_any(),
}
.into()
} else {
None
};
.into_any(),
ToolCallStatus::Pending | ToolCallStatus::InProgress
if is_edit
&& tool_call.content.is_empty()
&& self.as_native_connection(cx).is_some() =>
{
self.render_diff_loading(cx).into_any()
}
ToolCallStatus::Pending
| ToolCallStatus::InProgress
| ToolCallStatus::Completed
| ToolCallStatus::Failed
| ToolCallStatus::Canceled => v_flex()
.w_full()
.children(tool_call.content.iter().enumerate().map(
|(content_ix, content)| {
div().child(self.render_tool_call_content(
entry_ix,
content,
content_ix,
tool_call,
use_card_layout,
window,
cx,
))
},
))
.into_any(),
ToolCallStatus::Rejected => Empty.into_any(),
}
.into()
} else {
None
};
v_flex()
.map(|this| {
@ -2282,6 +2327,7 @@ impl AcpThreadView {
&self,
entry_ix: usize,
content: &ToolCallContent,
context_ix: usize,
tool_call: &ToolCall,
card_layout: bool,
window: &Window,
@ -2295,6 +2341,7 @@ impl AcpThreadView {
self.render_markdown_output(
markdown.clone(),
tool_call.id.clone(),
context_ix,
card_layout,
window,
cx,
@ -2314,6 +2361,7 @@ impl AcpThreadView {
&self,
markdown: Entity<Markdown>,
tool_call_id: acp::ToolCallId,
context_ix: usize,
card_layout: bool,
window: &Window,
cx: &Context<Self>,
@ -2330,11 +2378,13 @@ impl AcpThreadView {
.border_color(self.tool_card_border_color(cx))
})
.when(card_layout, |this| {
this.p_2()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
this.px_2().pb_2().when(context_ix > 0, |this| {
this.border_t_1()
.pt_2()
.border_color(self.tool_card_border_color(cx))
})
})
.text_sm()
.text_xs()
.text_color(cx.theme().colors().text_muted)
.child(self.render_markdown(markdown, default_markdown_style(false, false, window, cx)))
.when(!card_layout, |this| {
@ -2415,6 +2465,7 @@ impl AcpThreadView {
fn render_permission_buttons(
&self,
kind: acp::ToolKind,
options: &[acp::PermissionOption],
entry_ix: usize,
tool_call_id: acp::ToolCallId,
@ -2422,53 +2473,65 @@ impl AcpThreadView {
) -> Div {
h_flex()
.py_1()
.pl_2()
.pr_1()
.px_1()
.gap_1()
.justify_between()
.flex_wrap()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
.child(
.when(kind != acp::ToolKind::SwitchMode, |this| {
this.pl_2().child(
div().min_w(rems_from_px(145.)).child(
LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small),
),
)
})
.child({
div()
.min_w(rems_from_px(145.))
.child(LoadingLabel::new("Waiting for Confirmation").size(LabelSize::Small)),
)
.child(h_flex().gap_0p5().children(options.iter().map(|option| {
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| match option.kind {
acp::PermissionOptionKind::AllowOnce => {
this.icon(IconName::Check).icon_color(Color::Success)
}
acp::PermissionOptionKind::AllowAlways => {
this.icon(IconName::CheckDouble).icon_color(Color::Success)
}
acp::PermissionOptionKind::RejectOnce => {
this.icon(IconName::Close).icon_color(Color::Error)
}
acp::PermissionOptionKind::RejectAlways => {
this.icon(IconName::Close).icon_color(Color::Error)
.map(|this| {
if kind == acp::ToolKind::SwitchMode {
this.w_full().v_flex()
} else {
this.h_flex()
}
})
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
let option_id = option.id.clone();
let option_kind = option.kind;
move |this, _, window, cx| {
this.authorize_tool_call(
tool_call_id.clone(),
option_id.clone(),
option_kind,
window,
cx,
);
}
.gap_0p5()
.children(options.iter().map(|option| {
let option_id = SharedString::from(option.id.0.clone());
Button::new((option_id, entry_ix), option.name.clone())
.map(|this| match option.kind {
acp::PermissionOptionKind::AllowOnce => {
this.icon(IconName::Check).icon_color(Color::Success)
}
acp::PermissionOptionKind::AllowAlways => {
this.icon(IconName::CheckDouble).icon_color(Color::Success)
}
acp::PermissionOptionKind::RejectOnce => {
this.icon(IconName::Close).icon_color(Color::Error)
}
acp::PermissionOptionKind::RejectAlways => {
this.icon(IconName::Close).icon_color(Color::Error)
}
})
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
.label_size(LabelSize::Small)
.on_click(cx.listener({
let tool_call_id = tool_call_id.clone();
let option_id = option.id.clone();
let option_kind = option.kind;
move |this, _, window, cx| {
this.authorize_tool_call(
tool_call_id.clone(),
option_id.clone(),
option_kind,
window,
cx,
);
}
}))
}))
})))
})
}
fn render_diff_loading(&self, cx: &Context<Self>) -> AnyElement {
@ -3729,6 +3792,15 @@ impl AcpThreadView {
.on_action(cx.listener(|this, _: &ToggleProfileSelector, window, cx| {
if let Some(profile_selector) = this.profile_selector.as_ref() {
profile_selector.read(cx).menu_handle().toggle(window, cx);
} else if let Some(mode_selector) = this.mode_selector() {
mode_selector.read(cx).menu_handle().toggle(window, cx);
}
}))
.on_action(cx.listener(|this, _: &CycleModeSelector, window, cx| {
if let Some(mode_selector) = this.mode_selector() {
mode_selector.update(cx, |mode_selector, cx| {
mode_selector.cycle_mode(window, cx);
});
}
}))
.on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
@ -3795,6 +3867,7 @@ 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())
.child(self.render_send_button(cx)),
),

View file

@ -1322,6 +1322,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
args: vec![],
env: Some(HashMap::default()),
},
default_mode: None,
},
);
}

View file

@ -1529,7 +1529,8 @@ impl AgentDiff {
| AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::AvailableCommandsUpdated(_)
| AcpThreadEvent::Retry(_) => {}
| AcpThreadEvent::Retry(_)
| AcpThreadEvent::ModeUpdated(_) => {}
}
}

View file

@ -2717,10 +2717,13 @@ impl AgentPanel {
panel.update(cx, |panel, cx| {
panel.new_agent_thread(
AgentType::Custom {
name: agent_name
.clone()
.into(),
command: custom_settings.get(&agent_name.0).map(|settings| settings.command.clone()).unwrap_or(placeholder_command())
name: agent_name.clone().into(),
command: custom_settings
.get(&agent_name.0)
.map(|settings| {
settings.command.clone()
})
.unwrap_or(placeholder_command()),
},
window,
cx,

View file

@ -72,8 +72,10 @@ actions!(
ToggleOptionsMenu,
/// Deletes the recently opened thread from history.
DeleteRecentlyOpenThread,
/// Toggles the profile selector for switching between agent profiles.
/// Toggles the profile or mode selector for switching between agent profiles.
ToggleProfileSelector,
/// Cycles through available session modes.
CycleModeSelector,
/// Removes all added context from the current conversation.
RemoveAllContext,
/// Expands the message editor to full size.

View file

@ -191,7 +191,10 @@ impl AgentServerStore {
fs: fs.clone(),
node_runtime: node_runtime.clone(),
project_environment: project_environment.clone(),
custom_command: new_settings.claude.clone().map(|settings| settings.command),
custom_command: new_settings
.claude
.clone()
.and_then(|settings| settings.custom_command()),
}),
);
self.external_agents
@ -997,7 +1000,7 @@ pub const CLAUDE_CODE_NAME: &'static str = "claude";
#[settings_key(key = "agent_servers")]
pub struct AllAgentServersSettings {
pub gemini: Option<BuiltinAgentServerSettings>,
pub claude: Option<CustomAgentServerSettings>,
pub claude: Option<BuiltinAgentServerSettings>,
/// Custom agent servers configured by the user
#[serde(flatten)]
@ -1027,6 +1030,12 @@ pub struct BuiltinAgentServerSettings {
///
/// Default: true
pub ignore_system_version: Option<bool>,
/// The default mode to use for this agent.
///
/// Note: Not only all agents support modes.
///
/// Default: None
pub default_mode: Option<String>,
}
impl BuiltinAgentServerSettings {
@ -1054,6 +1063,12 @@ impl From<AgentServerCommand> for BuiltinAgentServerSettings {
pub struct CustomAgentServerSettings {
#[serde(flatten)]
pub command: AgentServerCommand,
/// The default mode to use for this agent.
///
/// Note: Not only all agents support modes.
///
/// Default: None
pub default_mode: Option<String>,
}
impl settings::Settings for AllAgentServersSettings {