Detailed changes
@@ -289,7 +289,6 @@ dependencies = [
"action_log",
"agent-client-protocol",
"agent_settings",
- "agentic-coding-protocol",
"anyhow",
"client",
"collections",
@@ -443,24 +442,6 @@ dependencies = [
"zed_actions",
]
-[[package]]
-name = "agentic-coding-protocol"
-version = "0.0.10"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "a3e6ae951b36fa2f8d9dd6e1af6da2fcaba13d7c866cf6a9e65deda9dc6c5fe4"
-dependencies = [
- "anyhow",
- "chrono",
- "derive_more 2.0.1",
- "futures 0.3.31",
- "log",
- "parking_lot",
- "schemars",
- "semver",
- "serde",
- "serde_json",
-]
-
[[package]]
name = "ahash"
version = "0.7.8"
@@ -876,7 +857,7 @@ dependencies = [
"anyhow",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"extension",
"futures 0.3.31",
"gpui",
@@ -939,7 +920,7 @@ dependencies = [
"clock",
"collections",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"icons",
"indoc",
@@ -976,7 +957,7 @@ dependencies = [
"cloud_llm_client",
"collections",
"component",
- "derive_more 0.99.19",
+ "derive_more",
"diffy",
"editor",
"feature_flags",
@@ -3089,7 +3070,7 @@ dependencies = [
"cocoa 0.26.0",
"collections",
"credentials_provider",
- "derive_more 0.99.19",
+ "derive_more",
"feature_flags",
"fs",
"futures 0.3.31",
@@ -3521,7 +3502,7 @@ name = "command_palette_hooks"
version = "0.1.0"
dependencies = [
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"gpui",
"workspace-hack",
]
@@ -4684,27 +4665,6 @@ dependencies = [
"syn 2.0.101",
]
-[[package]]
-name = "derive_more"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "093242cf7570c207c83073cf82f79706fe7b8317e98620a47d5be7c3d8497678"
-dependencies = [
- "derive_more-impl",
-]
-
-[[package]]
-name = "derive_more-impl"
-version = "2.0.1"
-source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "bda628edc44c4bb645fbe0f758797143e4e07926f7ebf4e9bdfbd3d2ce621df3"
-dependencies = [
- "proc-macro2",
- "quote",
- "syn 2.0.101",
- "unicode-xid",
-]
-
[[package]]
name = "derive_refineable"
version = "0.1.0"
@@ -6441,7 +6401,7 @@ dependencies = [
"askpass",
"async-trait",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"futures 0.3.31",
"git2",
"gpui",
@@ -7471,7 +7431,7 @@ dependencies = [
"core-video",
"cosmic-text",
"ctor",
- "derive_more 0.99.19",
+ "derive_more",
"embed-resource",
"env_logger 0.11.8",
"etagere",
@@ -7996,7 +7956,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"bytes 1.10.1",
- "derive_more 0.99.19",
+ "derive_more",
"futures 0.3.31",
"http 1.3.1",
"http-body 1.0.1",
@@ -14399,12 +14359,10 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
- "chrono",
"dyn-clone",
"indexmap",
"ref-cast",
"schemars_derive",
- "semver",
"serde",
"serde_json",
]
@@ -16488,7 +16446,7 @@ version = "0.1.0"
dependencies = [
"anyhow",
"collections",
- "derive_more 0.99.19",
+ "derive_more",
"fs",
"futures 0.3.31",
"gpui",
@@ -20003,7 +19961,6 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
- "schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -426,7 +426,6 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
-agentic-coding-protocol = "0.0.10"
agent-client-protocol = "0.0.31"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
@@ -22,7 +22,6 @@ acp_thread.workspace = true
action_log.workspace = true
agent-client-protocol.workspace = true
agent_settings.workspace = true
-agentic-coding-protocol.workspace = true
anyhow.workspace = true
client = { workspace = true, optional = true }
collections.workspace = true
@@ -1,34 +1,391 @@
-use std::{path::Path, rc::Rc};
-
use crate::AgentServerCommand;
use acp_thread::AgentConnection;
-use anyhow::Result;
-use gpui::AsyncApp;
+use acp_tools::AcpConnectionRegistry;
+use action_log::ActionLog;
+use agent_client_protocol::{self as acp, Agent as _, ErrorCode};
+use anyhow::anyhow;
+use collections::HashMap;
+use futures::AsyncBufReadExt as _;
+use futures::channel::oneshot;
+use futures::io::BufReader;
+use project::Project;
+use serde::Deserialize;
+use std::{any::Any, cell::RefCell};
+use std::{path::Path, rc::Rc};
use thiserror::Error;
-mod v0;
-mod v1;
+use anyhow::{Context as _, Result};
+use gpui::{App, AppContext as _, AsyncApp, Entity, Task, WeakEntity};
+
+use acp_thread::{AcpThread, AuthRequired, LoadError};
#[derive(Debug, Error)]
#[error("Unsupported version")]
pub struct UnsupportedVersion;
+pub struct AcpConnection {
+ server_name: &'static str,
+ connection: Rc<acp::ClientSideConnection>,
+ sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
+ auth_methods: Vec<acp::AuthMethod>,
+ prompt_capabilities: acp::PromptCapabilities,
+ _io_task: Task<Result<()>>,
+}
+
+pub struct AcpSession {
+ thread: WeakEntity<AcpThread>,
+ suppress_abort_err: bool,
+}
+
pub async fn connect(
server_name: &'static str,
command: AgentServerCommand,
root_dir: &Path,
cx: &mut AsyncApp,
) -> Result<Rc<dyn AgentConnection>> {
- let conn = v1::AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await;
-
- match conn {
- Ok(conn) => Ok(Rc::new(conn) as _),
- Err(err) if err.is::<UnsupportedVersion>() => {
- // Consider re-using initialize response and subprocess when adding another version here
- let conn: Rc<dyn AgentConnection> =
- Rc::new(v0::AcpConnection::stdio(server_name, command, root_dir, cx).await?);
- Ok(conn)
+ let conn = AcpConnection::stdio(server_name, command.clone(), root_dir, cx).await?;
+ Ok(Rc::new(conn) as _)
+}
+
+const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
+
+impl AcpConnection {
+ pub async fn stdio(
+ server_name: &'static str,
+ command: AgentServerCommand,
+ root_dir: &Path,
+ cx: &mut AsyncApp,
+ ) -> Result<Self> {
+ let mut child = util::command::new_smol_command(&command.path)
+ .args(command.args.iter().map(|arg| arg.as_str()))
+ .envs(command.env.iter().flatten())
+ .current_dir(root_dir)
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::piped())
+ .kill_on_drop(true)
+ .spawn()?;
+
+ let stdout = child.stdout.take().context("Failed to take stdout")?;
+ let stdin = child.stdin.take().context("Failed to take stdin")?;
+ let stderr = child.stderr.take().context("Failed to take stderr")?;
+ log::trace!("Spawned (pid: {})", child.id());
+
+ let sessions = Rc::new(RefCell::new(HashMap::default()));
+
+ let client = ClientDelegate {
+ sessions: sessions.clone(),
+ cx: cx.clone(),
+ };
+ let (connection, io_task) = acp::ClientSideConnection::new(client, stdin, stdout, {
+ let foreground_executor = cx.foreground_executor().clone();
+ move |fut| {
+ foreground_executor.spawn(fut).detach();
+ }
+ });
+
+ let io_task = cx.background_spawn(io_task);
+
+ cx.background_spawn(async move {
+ let mut stderr = BufReader::new(stderr);
+ let mut line = String::new();
+ while let Ok(n) = stderr.read_line(&mut line).await
+ && n > 0
+ {
+ log::warn!("agent stderr: {}", &line);
+ line.clear();
+ }
+ })
+ .detach();
+
+ cx.spawn({
+ let sessions = sessions.clone();
+ async move |cx| {
+ let status = child.status().await?;
+
+ for session in sessions.borrow().values() {
+ session
+ .thread
+ .update(cx, |thread, cx| {
+ thread.emit_load_error(LoadError::Exited { status }, cx)
+ })
+ .ok();
+ }
+
+ anyhow::Ok(())
+ }
+ })
+ .detach();
+
+ let connection = Rc::new(connection);
+
+ cx.update(|cx| {
+ AcpConnectionRegistry::default_global(cx).update(cx, |registry, cx| {
+ registry.set_active_connection(server_name, &connection, cx)
+ });
+ })?;
+
+ let response = connection
+ .initialize(acp::InitializeRequest {
+ protocol_version: acp::VERSION,
+ client_capabilities: acp::ClientCapabilities {
+ fs: acp::FileSystemCapability {
+ read_text_file: true,
+ write_text_file: true,
+ },
+ },
+ })
+ .await?;
+
+ if response.protocol_version < MINIMUM_SUPPORTED_VERSION {
+ return Err(UnsupportedVersion.into());
+ }
+
+ Ok(Self {
+ auth_methods: response.auth_methods,
+ connection,
+ server_name,
+ sessions,
+ prompt_capabilities: response.agent_capabilities.prompt_capabilities,
+ _io_task: io_task,
+ })
+ }
+}
+
+impl AgentConnection for AcpConnection {
+ fn new_thread(
+ self: Rc<Self>,
+ project: Entity<Project>,
+ cwd: &Path,
+ cx: &mut App,
+ ) -> Task<Result<Entity<AcpThread>>> {
+ let conn = self.connection.clone();
+ let sessions = self.sessions.clone();
+ let cwd = cwd.to_path_buf();
+ cx.spawn(async move |cx| {
+ let response = conn
+ .new_session(acp::NewSessionRequest {
+ mcp_servers: vec![],
+ cwd,
+ })
+ .await
+ .map_err(|err| {
+ if err.code == acp::ErrorCode::AUTH_REQUIRED.code {
+ let mut error = AuthRequired::new();
+
+ if err.message != acp::ErrorCode::AUTH_REQUIRED.message {
+ error = error.with_description(err.message);
+ }
+
+ anyhow!(error)
+ } else {
+ anyhow!(err)
+ }
+ })?;
+
+ let session_id = response.session_id;
+ let action_log = cx.new(|_| ActionLog::new(project.clone()))?;
+ let thread = cx.new(|_cx| {
+ AcpThread::new(
+ self.server_name,
+ self.clone(),
+ project,
+ action_log,
+ session_id.clone(),
+ )
+ })?;
+
+ let session = AcpSession {
+ thread: thread.downgrade(),
+ suppress_abort_err: false,
+ };
+ sessions.borrow_mut().insert(session_id, session);
+
+ Ok(thread)
+ })
+ }
+
+ fn auth_methods(&self) -> &[acp::AuthMethod] {
+ &self.auth_methods
+ }
+
+ fn authenticate(&self, method_id: acp::AuthMethodId, cx: &mut App) -> Task<Result<()>> {
+ let conn = self.connection.clone();
+ cx.foreground_executor().spawn(async move {
+ let result = conn
+ .authenticate(acp::AuthenticateRequest {
+ method_id: method_id.clone(),
+ })
+ .await?;
+
+ Ok(result)
+ })
+ }
+
+ fn prompt(
+ &self,
+ _id: Option<acp_thread::UserMessageId>,
+ params: acp::PromptRequest,
+ cx: &mut App,
+ ) -> Task<Result<acp::PromptResponse>> {
+ let conn = self.connection.clone();
+ let sessions = self.sessions.clone();
+ let session_id = params.session_id.clone();
+ cx.foreground_executor().spawn(async move {
+ let result = conn.prompt(params).await;
+
+ let mut suppress_abort_err = false;
+
+ if let Some(session) = sessions.borrow_mut().get_mut(&session_id) {
+ suppress_abort_err = session.suppress_abort_err;
+ session.suppress_abort_err = false;
+ }
+
+ match result {
+ Ok(response) => Ok(response),
+ Err(err) => {
+ if err.code != ErrorCode::INTERNAL_ERROR.code {
+ anyhow::bail!(err)
+ }
+
+ let Some(data) = &err.data else {
+ anyhow::bail!(err)
+ };
+
+ // Temporary workaround until the following PR is generally available:
+ // https://github.com/google-gemini/gemini-cli/pull/6656
+
+ #[derive(Deserialize)]
+ #[serde(deny_unknown_fields)]
+ struct ErrorDetails {
+ details: Box<str>,
+ }
+
+ match serde_json::from_value(data.clone()) {
+ Ok(ErrorDetails { details }) => {
+ if suppress_abort_err && details.contains("This operation was aborted")
+ {
+ Ok(acp::PromptResponse {
+ stop_reason: acp::StopReason::Cancelled,
+ })
+ } else {
+ Err(anyhow!(details))
+ }
+ }
+ Err(_) => Err(anyhow!(err)),
+ }
+ }
+ }
+ })
+ }
+
+ fn prompt_capabilities(&self) -> acp::PromptCapabilities {
+ self.prompt_capabilities
+ }
+
+ fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
+ if let Some(session) = self.sessions.borrow_mut().get_mut(session_id) {
+ session.suppress_abort_err = true;
}
- Err(err) => Err(err),
+ let conn = self.connection.clone();
+ let params = acp::CancelNotification {
+ session_id: session_id.clone(),
+ };
+ cx.foreground_executor()
+ .spawn(async move { conn.cancel(params).await })
+ .detach();
+ }
+
+ fn into_any(self: Rc<Self>) -> Rc<dyn Any> {
+ self
+ }
+}
+
+struct ClientDelegate {
+ sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
+ cx: AsyncApp,
+}
+
+impl acp::Client for ClientDelegate {
+ async fn request_permission(
+ &self,
+ arguments: acp::RequestPermissionRequest,
+ ) -> Result<acp::RequestPermissionResponse, acp::Error> {
+ let cx = &mut self.cx.clone();
+ let rx = self
+ .sessions
+ .borrow()
+ .get(&arguments.session_id)
+ .context("Failed to get session")?
+ .thread
+ .update(cx, |thread, cx| {
+ thread.request_tool_call_authorization(arguments.tool_call, arguments.options, cx)
+ })?;
+
+ let result = rx?.await;
+
+ let outcome = match result {
+ Ok(option) => acp::RequestPermissionOutcome::Selected { option_id: option },
+ Err(oneshot::Canceled) => acp::RequestPermissionOutcome::Cancelled,
+ };
+
+ Ok(acp::RequestPermissionResponse { outcome })
+ }
+
+ async fn write_text_file(
+ &self,
+ arguments: acp::WriteTextFileRequest,
+ ) -> Result<(), acp::Error> {
+ let cx = &mut self.cx.clone();
+ let task = self
+ .sessions
+ .borrow()
+ .get(&arguments.session_id)
+ .context("Failed to get session")?
+ .thread
+ .update(cx, |thread, cx| {
+ thread.write_text_file(arguments.path, arguments.content, cx)
+ })?;
+
+ task.await?;
+
+ Ok(())
+ }
+
+ async fn read_text_file(
+ &self,
+ arguments: acp::ReadTextFileRequest,
+ ) -> Result<acp::ReadTextFileResponse, acp::Error> {
+ let cx = &mut self.cx.clone();
+ let task = self
+ .sessions
+ .borrow()
+ .get(&arguments.session_id)
+ .context("Failed to get session")?
+ .thread
+ .update(cx, |thread, cx| {
+ thread.read_text_file(arguments.path, arguments.line, arguments.limit, false, cx)
+ })?;
+
+ let content = task.await?;
+
+ Ok(acp::ReadTextFileResponse { content })
+ }
+
+ async fn session_notification(
+ &self,
+ notification: acp::SessionNotification,
+ ) -> Result<(), acp::Error> {
+ let cx = &mut self.cx.clone();
+ let sessions = self.sessions.borrow();
+ let session = sessions
+ .get(¬ification.session_id)
+ .context("Failed to get session")?;
+
+ session.thread.update(cx, |thread, cx| {
+ thread.handle_session_update(notification.update, cx)
+ })??;
+
+ Ok(())
}
}
@@ -109,7 +109,6 @@ rustc-hash = { version = "1" }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
-schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
semver = { version = "1", features = ["serde"] }
@@ -244,7 +243,6 @@ rustc-hash = { version = "1" }
rustix-d585fab2519d2d1 = { package = "rustix", version = "0.38", default-features = false, features = ["fs", "net", "std"] }
rustls = { version = "0.23", features = ["ring"] }
rustls-webpki = { version = "0.103", default-features = false, features = ["aws-lc-rs", "ring", "std"] }
-schemars = { version = "1", features = ["chrono04", "indexmap2", "semver1"] }
sea-orm = { version = "1", features = ["runtime-tokio-rustls", "sqlx-postgres", "sqlx-sqlite"] }
sea-query-binder = { version = "0.7", default-features = false, features = ["postgres-array", "sqlx-postgres", "sqlx-sqlite", "with-bigdecimal", "with-chrono", "with-json", "with-rust_decimal", "with-time", "with-uuid"] }
semver = { version = "1", features = ["serde"] }