Detailed changes
@@ -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",
@@ -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"
@@ -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"
}
},
{
@@ -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"
}
},
{
@@ -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"
}
},
{
@@ -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,
@@ -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
@@ -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| {
@@ -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,
)
})??
@@ -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 } =
@@ -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(¬ification.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(¬ification.session_id)
+ .context("Failed to get session")?;
+
+ if let acp::SessionUpdate::CurrentModeUpdate { current_mode_id } = ¬ification.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(())
}
@@ -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,
@@ -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))
})
}
@@ -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))
})
}
@@ -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(),
@@ -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))
})
}
@@ -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) {}
+}
@@ -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.
@@ -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::*;
@@ -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 == ¤t_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)))
+ })
+ }
+}
@@ -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)),
),
@@ -1322,6 +1322,7 @@ async fn open_new_agent_servers_entry_in_settings_editor(
args: vec![],
env: Some(HashMap::default()),
},
+ default_mode: None,
},
);
}
@@ -1529,7 +1529,8 @@ impl AgentDiff {
| AcpThreadEvent::ToolAuthorizationRequired
| AcpThreadEvent::PromptCapabilitiesUpdated
| AcpThreadEvent::AvailableCommandsUpdated(_)
- | AcpThreadEvent::Retry(_) => {}
+ | AcpThreadEvent::Retry(_)
+ | AcpThreadEvent::ModeUpdated(_) => {}
}
}
@@ -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,
@@ -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.
@@ -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 {