diff --git a/Cargo.lock b/Cargo.lock
index 5c22b90526..0815155ee6 100644
--- a/Cargo.lock
+++ b/Cargo.lock
@@ -2,6 +2,33 @@
# It is not intended for manual editing.
version = 4
+[[package]]
+name = "acp"
+version = "0.1.0"
+dependencies = [
+ "agent_servers",
+ "agentic-coding-protocol",
+ "anyhow",
+ "async-pipe",
+ "buffer_diff",
+ "editor",
+ "env_logger 0.11.8",
+ "futures 0.3.31",
+ "gpui",
+ "indoc",
+ "itertools 0.14.0",
+ "language",
+ "markdown",
+ "project",
+ "serde_json",
+ "settings",
+ "smol",
+ "tempfile",
+ "ui",
+ "util",
+ "workspace-hack",
+]
+
[[package]]
name = "activity_indicator"
version = "0.1.0"
@@ -107,6 +134,24 @@ dependencies = [
"zstd",
]
+[[package]]
+name = "agent_servers"
+version = "0.1.0"
+dependencies = [
+ "anyhow",
+ "collections",
+ "futures 0.3.31",
+ "gpui",
+ "paths",
+ "project",
+ "schemars",
+ "serde",
+ "settings",
+ "util",
+ "which 6.0.3",
+ "workspace-hack",
+]
+
[[package]]
name = "agent_settings"
version = "0.1.0"
@@ -130,8 +175,11 @@ dependencies = [
name = "agent_ui"
version = "0.1.0"
dependencies = [
+ "acp",
"agent",
+ "agent_servers",
"agent_settings",
+ "agentic-coding-protocol",
"anyhow",
"assistant_context",
"assistant_slash_command",
@@ -191,6 +239,7 @@ dependencies = [
"settings",
"smol",
"streaming_diff",
+ "task",
"telemetry",
"telemetry_events",
"terminal",
@@ -212,6 +261,22 @@ dependencies = [
"zed_llm_client",
]
+[[package]]
+name = "agentic-coding-protocol"
+version = "0.0.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "b962eee17ee3924870d9b9d28cc8b6dcb5421e4d4e81cd864226374a122ceed1"
+dependencies = [
+ "anyhow",
+ "chrono",
+ "futures 0.3.31",
+ "log",
+ "parking_lot",
+ "schemars",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "ahash"
version = "0.7.8"
@@ -14078,6 +14143,7 @@ version = "1.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fe8c9d1c68d67dd9f97ecbc6f932b60eb289c5dbddd8aa1405484a8fd2fcd984"
dependencies = [
+ "chrono",
"dyn-clone",
"indexmap",
"ref-cast",
@@ -19579,6 +19645,7 @@ dependencies = [
"rustix 1.0.7",
"rustls 0.23.26",
"rustls-webpki 0.103.1",
+ "schemars",
"scopeguard",
"sea-orm",
"sea-query-binder",
@@ -19976,6 +20043,7 @@ version = "0.196.0"
dependencies = [
"activity_indicator",
"agent",
+ "agent_servers",
"agent_settings",
"agent_ui",
"anyhow",
diff --git a/Cargo.toml b/Cargo.toml
index a4d8b3cb95..aac12b7ff8 100644
--- a/Cargo.toml
+++ b/Cargo.toml
@@ -2,9 +2,11 @@
resolver = "2"
members = [
"crates/activity_indicator",
+ "crates/acp",
"crates/agent_ui",
"crates/agent",
"crates/agent_settings",
+ "crates/agent_servers",
"crates/anthropic",
"crates/askpass",
"crates/assets",
@@ -216,10 +218,12 @@ edition = "2024"
# Workspace member crates
#
-activity_indicator = { path = "crates/activity_indicator" }
+acp = { path = "crates/acp" }
agent = { path = "crates/agent" }
+activity_indicator = { path = "crates/activity_indicator" }
agent_ui = { path = "crates/agent_ui" }
agent_settings = { path = "crates/agent_settings" }
+agent_servers = { path = "crates/agent_servers" }
ai = { path = "crates/ai" }
anthropic = { path = "crates/anthropic" }
askpass = { path = "crates/askpass" }
@@ -400,6 +404,7 @@ zlog_settings = { path = "crates/zlog_settings" }
# External crates
#
+agentic-coding-protocol = "0.0.5"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
diff --git a/assets/icons/ai_gemini.svg b/assets/icons/ai_gemini.svg
new file mode 100644
index 0000000000..60197dc4ad
--- /dev/null
+++ b/assets/icons/ai_gemini.svg
@@ -0,0 +1 @@
+
diff --git a/assets/icons/tool_bulb.svg b/assets/icons/tool_bulb.svg
new file mode 100644
index 0000000000..54d5ac5fd7
--- /dev/null
+++ b/assets/icons/tool_bulb.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/tool_folder.svg b/assets/icons/tool_folder.svg
new file mode 100644
index 0000000000..9d3ac299d2
--- /dev/null
+++ b/assets/icons/tool_folder.svg
@@ -0,0 +1,3 @@
+
diff --git a/assets/icons/tool_hammer.svg b/assets/icons/tool_hammer.svg
new file mode 100644
index 0000000000..e66173ce70
--- /dev/null
+++ b/assets/icons/tool_hammer.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/tool_pencil.svg b/assets/icons/tool_pencil.svg
new file mode 100644
index 0000000000..b913015c08
--- /dev/null
+++ b/assets/icons/tool_pencil.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/tool_regex.svg b/assets/icons/tool_regex.svg
new file mode 100644
index 0000000000..0432cd570f
--- /dev/null
+++ b/assets/icons/tool_regex.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/tool_search.svg b/assets/icons/tool_search.svg
new file mode 100644
index 0000000000..4f2750cfa2
--- /dev/null
+++ b/assets/icons/tool_search.svg
@@ -0,0 +1,4 @@
+
diff --git a/assets/icons/tool_terminal.svg b/assets/icons/tool_terminal.svg
new file mode 100644
index 0000000000..5154fa8e70
--- /dev/null
+++ b/assets/icons/tool_terminal.svg
@@ -0,0 +1,5 @@
+
diff --git a/assets/icons/tool_web.svg b/assets/icons/tool_web.svg
new file mode 100644
index 0000000000..49e9544b4a
--- /dev/null
+++ b/assets/icons/tool_web.svg
@@ -0,0 +1,17 @@
+
diff --git a/assets/keymaps/default-linux.json b/assets/keymaps/default-linux.json
index 6f50945828..a19bc77dcc 100644
--- a/assets/keymaps/default-linux.json
+++ b/assets/keymaps/default-linux.json
@@ -306,6 +306,15 @@
"enter": "agent::AcceptSuggestedContext"
}
},
+ {
+ "context": "AcpThread > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "up": "agent::PreviousHistoryMessage",
+ "down": "agent::NextHistoryMessage"
+ }
+ },
{
"context": "ThreadHistory",
"bindings": {
diff --git a/assets/keymaps/default-macos.json b/assets/keymaps/default-macos.json
index cbc90c05e6..875658c5a0 100644
--- a/assets/keymaps/default-macos.json
+++ b/assets/keymaps/default-macos.json
@@ -357,6 +357,15 @@
"ctrl--": "pane::GoBack"
}
},
+ {
+ "context": "AcpThread > Editor",
+ "use_key_equivalents": true,
+ "bindings": {
+ "enter": "agent::Chat",
+ "up": "agent::PreviousHistoryMessage",
+ "down": "agent::NextHistoryMessage"
+ }
+ },
{
"context": "ThreadHistory",
"bindings": {
diff --git a/assets/settings/default.json b/assets/settings/default.json
index 8c105b2c1e..08daa236ab 100644
--- a/assets/settings/default.json
+++ b/assets/settings/default.json
@@ -1855,6 +1855,8 @@
"read_ssh_config": true,
// Configures context servers for use by the agent.
"context_servers": {},
+ // Configures agent servers available in the agent panel.
+ "agent_servers": {},
"debugger": {
"stepping_granularity": "line",
"save_breakpoints": true,
diff --git a/crates/acp/Cargo.toml b/crates/acp/Cargo.toml
new file mode 100644
index 0000000000..dae6292e28
--- /dev/null
+++ b/crates/acp/Cargo.toml
@@ -0,0 +1,46 @@
+[package]
+name = "acp"
+version = "0.1.0"
+edition.workspace = true
+publish.workspace = true
+license = "GPL-3.0-or-later"
+
+[lints]
+workspace = true
+
+[lib]
+path = "src/acp.rs"
+doctest = false
+
+[features]
+test-support = ["gpui/test-support", "project/test-support"]
+gemini = []
+
+[dependencies]
+agent_servers.workspace = true
+agentic-coding-protocol.workspace = true
+anyhow.workspace = true
+buffer_diff.workspace = true
+editor.workspace = true
+futures.workspace = true
+gpui.workspace = true
+itertools.workspace = true
+language.workspace = true
+markdown.workspace = true
+project.workspace = true
+settings.workspace = true
+smol.workspace = true
+ui.workspace = true
+util.workspace = true
+workspace-hack.workspace = true
+
+[dev-dependencies]
+async-pipe.workspace = true
+env_logger.workspace = true
+gpui = { workspace = true, "features" = ["test-support"] }
+indoc.workspace = true
+project = { workspace = true, "features" = ["test-support"] }
+serde_json.workspace = true
+tempfile.workspace = true
+util.workspace = true
+settings.workspace = true
diff --git a/crates/acp/LICENSE-GPL b/crates/acp/LICENSE-GPL
new file mode 120000
index 0000000000..89e542f750
--- /dev/null
+++ b/crates/acp/LICENSE-GPL
@@ -0,0 +1 @@
+../../LICENSE-GPL
\ No newline at end of file
diff --git a/crates/acp/src/acp.rs b/crates/acp/src/acp.rs
new file mode 100644
index 0000000000..ce83618288
--- /dev/null
+++ b/crates/acp/src/acp.rs
@@ -0,0 +1,1625 @@
+pub use acp::ToolCallId;
+use agent_servers::AgentServer;
+use agentic_coding_protocol::{self as acp, UserMessageChunk};
+use anyhow::{Context as _, Result, anyhow};
+use buffer_diff::BufferDiff;
+use editor::{MultiBuffer, PathKey};
+use futures::{FutureExt, channel::oneshot, future::BoxFuture};
+use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
+use itertools::Itertools;
+use language::{Anchor, Buffer, Capability, LanguageRegistry, OffsetRangeExt as _};
+use markdown::Markdown;
+use project::Project;
+use std::error::Error;
+use std::fmt::{Formatter, Write};
+use std::{
+ fmt::Display,
+ mem,
+ path::{Path, PathBuf},
+ sync::Arc,
+};
+use ui::{App, IconName};
+use util::ResultExt;
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct UserMessage {
+ pub content: Entity,
+}
+
+impl UserMessage {
+ pub fn from_acp(
+ message: acp::UserMessage,
+ language_registry: Arc,
+ cx: &mut App,
+ ) -> Self {
+ let mut md_source = String::new();
+
+ for chunk in message.chunks {
+ match chunk {
+ UserMessageChunk::Text { chunk } => md_source.push_str(&chunk),
+ UserMessageChunk::Path { path } => {
+ write!(&mut md_source, "{}", MentionPath(&path)).unwrap()
+ }
+ }
+ }
+
+ Self {
+ content: cx
+ .new(|cx| Markdown::new(md_source.into(), Some(language_registry), None, cx)),
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ format!("## User\n\n{}\n\n", self.content.read(cx).source())
+ }
+}
+
+#[derive(Debug)]
+pub struct MentionPath<'a>(&'a Path);
+
+impl<'a> MentionPath<'a> {
+ const PREFIX: &'static str = "@file:";
+
+ pub fn new(path: &'a Path) -> Self {
+ MentionPath(path)
+ }
+
+ pub fn try_parse(url: &'a str) -> Option {
+ let path = url.strip_prefix(Self::PREFIX)?;
+ Some(MentionPath(Path::new(path)))
+ }
+
+ pub fn path(&self) -> &Path {
+ self.0
+ }
+}
+
+impl Display for MentionPath<'_> {
+ fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "[@{}]({}{})",
+ self.0.file_name().unwrap_or_default().display(),
+ Self::PREFIX,
+ self.0.display()
+ )
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub struct AssistantMessage {
+ pub chunks: Vec,
+}
+
+impl AssistantMessage {
+ fn to_markdown(&self, cx: &App) -> String {
+ format!(
+ "## Assistant\n\n{}\n\n",
+ self.chunks
+ .iter()
+ .map(|chunk| chunk.to_markdown(cx))
+ .join("\n\n")
+ )
+ }
+}
+
+#[derive(Clone, Debug, Eq, PartialEq)]
+pub enum AssistantMessageChunk {
+ Text { chunk: Entity },
+ Thought { chunk: Entity },
+}
+
+impl AssistantMessageChunk {
+ pub fn from_acp(
+ chunk: acp::AssistantMessageChunk,
+ language_registry: Arc,
+ cx: &mut App,
+ ) -> Self {
+ match chunk {
+ acp::AssistantMessageChunk::Text { chunk } => Self::Text {
+ chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
+ },
+ acp::AssistantMessageChunk::Thought { chunk } => Self::Thought {
+ chunk: cx.new(|cx| Markdown::new(chunk.into(), Some(language_registry), None, cx)),
+ },
+ }
+ }
+
+ pub fn from_str(chunk: &str, language_registry: Arc, cx: &mut App) -> Self {
+ Self::Text {
+ chunk: cx.new(|cx| {
+ Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
+ }),
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ match self {
+ Self::Text { chunk } => chunk.read(cx).source().to_string(),
+ Self::Thought { chunk } => {
+ format!("\n{}\n", chunk.read(cx).source())
+ }
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum AgentThreadEntry {
+ UserMessage(UserMessage),
+ AssistantMessage(AssistantMessage),
+ ToolCall(ToolCall),
+}
+
+impl AgentThreadEntry {
+ fn to_markdown(&self, cx: &App) -> String {
+ match self {
+ Self::UserMessage(message) => message.to_markdown(cx),
+ Self::AssistantMessage(message) => message.to_markdown(cx),
+ Self::ToolCall(too_call) => too_call.to_markdown(cx),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct ToolCall {
+ pub id: acp::ToolCallId,
+ pub label: Entity,
+ pub icon: IconName,
+ pub content: Option,
+ pub status: ToolCallStatus,
+}
+
+impl ToolCall {
+ fn to_markdown(&self, cx: &App) -> String {
+ let mut markdown = format!(
+ "**Tool Call: {}**\nStatus: {}\n\n",
+ self.label.read(cx).source(),
+ self.status
+ );
+ if let Some(content) = &self.content {
+ markdown.push_str(content.to_markdown(cx).as_str());
+ markdown.push_str("\n\n");
+ }
+ markdown
+ }
+}
+
+#[derive(Debug)]
+pub enum ToolCallStatus {
+ WaitingForConfirmation {
+ confirmation: ToolCallConfirmation,
+ respond_tx: oneshot::Sender,
+ },
+ Allowed {
+ status: acp::ToolCallStatus,
+ },
+ Rejected,
+ Canceled,
+}
+
+impl Display for ToolCallStatus {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ write!(
+ f,
+ "{}",
+ match self {
+ ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
+ ToolCallStatus::Allowed { status } => match status {
+ acp::ToolCallStatus::Running => "Running",
+ acp::ToolCallStatus::Finished => "Finished",
+ acp::ToolCallStatus::Error => "Error",
+ },
+ ToolCallStatus::Rejected => "Rejected",
+ ToolCallStatus::Canceled => "Canceled",
+ }
+ )
+ }
+}
+
+#[derive(Debug)]
+pub enum ToolCallConfirmation {
+ Edit {
+ description: Option>,
+ },
+ Execute {
+ command: String,
+ root_command: String,
+ description: Option>,
+ },
+ Mcp {
+ server_name: String,
+ tool_name: String,
+ tool_display_name: String,
+ description: Option>,
+ },
+ Fetch {
+ urls: Vec,
+ description: Option>,
+ },
+ Other {
+ description: Entity,
+ },
+}
+
+impl ToolCallConfirmation {
+ pub fn from_acp(
+ confirmation: acp::ToolCallConfirmation,
+ language_registry: Arc,
+ cx: &mut App,
+ ) -> Self {
+ let to_md = |description: String, cx: &mut App| -> Entity {
+ cx.new(|cx| {
+ Markdown::new(
+ description.into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ })
+ };
+
+ match confirmation {
+ acp::ToolCallConfirmation::Edit { description } => Self::Edit {
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Execute {
+ command,
+ root_command,
+ description,
+ } => Self::Execute {
+ command,
+ root_command,
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Mcp {
+ server_name,
+ tool_name,
+ tool_display_name,
+ description,
+ } => Self::Mcp {
+ server_name,
+ tool_name,
+ tool_display_name,
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Fetch { urls, description } => Self::Fetch {
+ urls: urls.iter().map(|url| url.into()).collect(),
+ description: description.map(|description| to_md(description, cx)),
+ },
+ acp::ToolCallConfirmation::Other { description } => Self::Other {
+ description: to_md(description, cx),
+ },
+ }
+ }
+}
+
+#[derive(Debug)]
+pub enum ToolCallContent {
+ Markdown { markdown: Entity },
+ Diff { diff: Diff },
+}
+
+impl ToolCallContent {
+ pub fn from_acp(
+ content: acp::ToolCallContent,
+ language_registry: Arc,
+ cx: &mut App,
+ ) -> Self {
+ match content {
+ acp::ToolCallContent::Markdown { markdown } => Self::Markdown {
+ markdown: cx.new(|cx| Markdown::new_text(markdown.into(), cx)),
+ },
+ acp::ToolCallContent::Diff { diff } => Self::Diff {
+ diff: Diff::from_acp(diff, language_registry, cx),
+ },
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ match self {
+ Self::Markdown { markdown } => markdown.read(cx).source().to_string(),
+ Self::Diff { diff } => diff.to_markdown(cx),
+ }
+ }
+}
+
+#[derive(Debug)]
+pub struct Diff {
+ pub multibuffer: Entity,
+ pub path: PathBuf,
+ _task: Task>,
+}
+
+impl Diff {
+ pub fn from_acp(
+ diff: acp::Diff,
+ language_registry: Arc,
+ cx: &mut App,
+ ) -> Self {
+ let acp::Diff {
+ path,
+ old_text,
+ new_text,
+ } = diff;
+
+ let multibuffer = cx.new(|_cx| MultiBuffer::without_headers(Capability::ReadOnly));
+
+ let new_buffer = cx.new(|cx| Buffer::local(new_text, cx));
+ let old_buffer = cx.new(|cx| Buffer::local(old_text.unwrap_or("".into()), cx));
+ let new_buffer_snapshot = new_buffer.read(cx).text_snapshot();
+ let old_buffer_snapshot = old_buffer.read(cx).snapshot();
+ let buffer_diff = cx.new(|cx| BufferDiff::new(&new_buffer_snapshot, cx));
+ let diff_task = buffer_diff.update(cx, |diff, cx| {
+ diff.set_base_text(
+ old_buffer_snapshot,
+ Some(language_registry.clone()),
+ new_buffer_snapshot,
+ cx,
+ )
+ });
+
+ let task = cx.spawn({
+ let multibuffer = multibuffer.clone();
+ let path = path.clone();
+ async move |cx| {
+ diff_task.await?;
+
+ multibuffer
+ .update(cx, |multibuffer, cx| {
+ let hunk_ranges = {
+ let buffer = new_buffer.read(cx);
+ let diff = buffer_diff.read(cx);
+ diff.hunks_intersecting_range(Anchor::MIN..Anchor::MAX, &buffer, cx)
+ .map(|diff_hunk| diff_hunk.buffer_range.to_point(&buffer))
+ .collect::>()
+ };
+
+ multibuffer.set_excerpts_for_path(
+ PathKey::for_buffer(&new_buffer, cx),
+ new_buffer.clone(),
+ hunk_ranges,
+ editor::DEFAULT_MULTIBUFFER_CONTEXT,
+ cx,
+ );
+ multibuffer.add_diff(buffer_diff.clone(), cx);
+ })
+ .log_err();
+
+ if let Some(language) = language_registry
+ .language_for_file_path(&path)
+ .await
+ .log_err()
+ {
+ new_buffer.update(cx, |buffer, cx| buffer.set_language(Some(language), cx))?;
+ }
+
+ anyhow::Ok(())
+ }
+ });
+
+ Self {
+ multibuffer,
+ path,
+ _task: task,
+ }
+ }
+
+ fn to_markdown(&self, cx: &App) -> String {
+ let buffer_text = self
+ .multibuffer
+ .read(cx)
+ .all_buffers()
+ .iter()
+ .map(|buffer| buffer.read(cx).text())
+ .join("\n");
+ format!("Diff: {}\n```\n{}\n```\n", self.path.display(), buffer_text)
+ }
+}
+
+pub struct AcpThread {
+ entries: Vec,
+ title: SharedString,
+ project: Entity,
+ send_task: Option>,
+ connection: Arc,
+ child_status: Option>>,
+ _io_task: Task<()>,
+}
+
+pub enum AcpThreadEvent {
+ NewEntry,
+ EntryUpdated(usize),
+}
+
+impl EventEmitter for AcpThread {}
+
+#[derive(PartialEq, Eq)]
+pub enum ThreadStatus {
+ Idle,
+ WaitingForToolConfirmation,
+ Generating,
+}
+
+#[derive(Debug, Clone)]
+pub enum LoadError {
+ Unsupported { current_version: SharedString },
+ Exited(i32),
+ Other(SharedString),
+}
+
+impl Display for LoadError {
+ fn fmt(&self, f: &mut Formatter<'_>) -> std::fmt::Result {
+ match self {
+ LoadError::Unsupported { current_version } => {
+ write!(
+ f,
+ "Your installed version of Gemini {} doesn't support the Agentic Coding Protocol (ACP).",
+ current_version
+ )
+ }
+ LoadError::Exited(status) => write!(f, "Server exited with status {}", status),
+ LoadError::Other(msg) => write!(f, "{}", msg),
+ }
+ }
+}
+
+impl Error for LoadError {}
+
+impl AcpThread {
+ pub async fn spawn(
+ server: impl AgentServer + 'static,
+ root_dir: &Path,
+ project: Entity,
+ cx: &mut AsyncApp,
+ ) -> Result> {
+ let command = match server.command(&project, cx).await {
+ Ok(command) => command,
+ Err(e) => return Err(anyhow!(LoadError::Other(format!("{e}").into()))),
+ };
+
+ let mut child = util::command::new_smol_command(&command.path)
+ .args(command.args.iter())
+ .current_dir(root_dir)
+ .stdin(std::process::Stdio::piped())
+ .stdout(std::process::Stdio::piped())
+ .stderr(std::process::Stdio::inherit())
+ .kill_on_drop(true)
+ .spawn()?;
+
+ let stdin = child.stdin.take().unwrap();
+ let stdout = child.stdout.take().unwrap();
+
+ cx.new(|cx| {
+ let foreground_executor = cx.foreground_executor().clone();
+
+ let (connection, io_fut) = acp::AgentConnection::connect_to_agent(
+ AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()),
+ stdin,
+ stdout,
+ move |fut| foreground_executor.spawn(fut).detach(),
+ );
+
+ let io_task = cx.background_spawn(async move {
+ io_fut.await.log_err();
+ });
+
+ let child_status = cx.background_spawn(async move {
+ match child.status().await {
+ Err(e) => Err(anyhow!(e)),
+ Ok(result) if result.success() => Ok(()),
+ Ok(result) => {
+ if let Some(version) = server.version(&command).await.log_err()
+ && !version.supported
+ {
+ Err(anyhow!(LoadError::Unsupported {
+ current_version: version.current_version
+ }))
+ } else {
+ Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
+ }
+ }
+ }
+ });
+
+ Self {
+ entries: Default::default(),
+ title: "ACP Thread".into(),
+ project,
+ send_task: None,
+ connection: Arc::new(connection),
+ child_status: Some(child_status),
+ _io_task: io_task,
+ }
+ })
+ }
+
+ #[cfg(test)]
+ pub fn fake(
+ stdin: async_pipe::PipeWriter,
+ stdout: async_pipe::PipeReader,
+ project: Entity,
+ cx: &mut Context,
+ ) -> Self {
+ let foreground_executor = cx.foreground_executor().clone();
+
+ let (connection, io_fut) = acp::AgentConnection::connect_to_agent(
+ AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async()),
+ stdin,
+ stdout,
+ move |fut| {
+ foreground_executor.spawn(fut).detach();
+ },
+ );
+
+ let io_task = cx.background_spawn({
+ async move {
+ io_fut.await.log_err();
+ }
+ });
+
+ Self {
+ entries: Default::default(),
+ title: "ACP Thread".into(),
+ project,
+ send_task: None,
+ connection: Arc::new(connection),
+ child_status: None,
+ _io_task: io_task,
+ }
+ }
+
+ pub fn title(&self) -> SharedString {
+ self.title.clone()
+ }
+
+ pub fn entries(&self) -> &[AgentThreadEntry] {
+ &self.entries
+ }
+
+ pub fn status(&self) -> ThreadStatus {
+ if self.send_task.is_some() {
+ if self.waiting_for_tool_confirmation() {
+ ThreadStatus::WaitingForToolConfirmation
+ } else {
+ ThreadStatus::Generating
+ }
+ } else {
+ ThreadStatus::Idle
+ }
+ }
+
+ pub fn push_entry(&mut self, entry: AgentThreadEntry, cx: &mut Context) {
+ self.entries.push(entry);
+ cx.emit(AcpThreadEvent::NewEntry);
+ }
+
+ pub fn push_assistant_chunk(
+ &mut self,
+ chunk: acp::AssistantMessageChunk,
+ cx: &mut Context,
+ ) {
+ let entries_len = self.entries.len();
+ if let Some(last_entry) = self.entries.last_mut()
+ && let AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) = last_entry
+ {
+ cx.emit(AcpThreadEvent::EntryUpdated(entries_len - 1));
+
+ match (chunks.last_mut(), &chunk) {
+ (
+ Some(AssistantMessageChunk::Text { chunk: old_chunk }),
+ acp::AssistantMessageChunk::Text { chunk: new_chunk },
+ )
+ | (
+ Some(AssistantMessageChunk::Thought { chunk: old_chunk }),
+ acp::AssistantMessageChunk::Thought { chunk: new_chunk },
+ ) => {
+ old_chunk.update(cx, |old_chunk, cx| {
+ old_chunk.append(&new_chunk, cx);
+ });
+ }
+ _ => {
+ chunks.push(AssistantMessageChunk::from_acp(
+ chunk,
+ self.project.read(cx).languages().clone(),
+ cx,
+ ));
+ }
+ }
+ } else {
+ let chunk = AssistantMessageChunk::from_acp(
+ chunk,
+ self.project.read(cx).languages().clone(),
+ cx,
+ );
+
+ self.push_entry(
+ AgentThreadEntry::AssistantMessage(AssistantMessage {
+ chunks: vec![chunk],
+ }),
+ cx,
+ );
+ }
+ }
+
+ pub fn request_tool_call(
+ &mut self,
+ label: String,
+ icon: acp::Icon,
+ content: Option,
+ confirmation: acp::ToolCallConfirmation,
+ cx: &mut Context,
+ ) -> ToolCallRequest {
+ let (tx, rx) = oneshot::channel();
+
+ let status = ToolCallStatus::WaitingForConfirmation {
+ confirmation: ToolCallConfirmation::from_acp(
+ confirmation,
+ self.project.read(cx).languages().clone(),
+ cx,
+ ),
+ respond_tx: tx,
+ };
+
+ let id = self.insert_tool_call(label, status, icon, content, cx);
+ ToolCallRequest { id, outcome: rx }
+ }
+
+ pub fn push_tool_call(
+ &mut self,
+ label: String,
+ icon: acp::Icon,
+ content: Option,
+ cx: &mut Context,
+ ) -> acp::ToolCallId {
+ let status = ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Running,
+ };
+
+ self.insert_tool_call(label, status, icon, content, cx)
+ }
+
+ fn insert_tool_call(
+ &mut self,
+ label: String,
+ status: ToolCallStatus,
+ icon: acp::Icon,
+ content: Option,
+ cx: &mut Context,
+ ) -> acp::ToolCallId {
+ let language_registry = self.project.read(cx).languages().clone();
+ let id = acp::ToolCallId(self.entries.len() as u64);
+
+ self.push_entry(
+ AgentThreadEntry::ToolCall(ToolCall {
+ id,
+ label: cx.new(|cx| {
+ Markdown::new(label.into(), Some(language_registry.clone()), None, cx)
+ }),
+ icon: acp_icon_to_ui_icon(icon),
+ content: content
+ .map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
+ status,
+ }),
+ cx,
+ );
+
+ id
+ }
+
+ pub fn authorize_tool_call(
+ &mut self,
+ id: acp::ToolCallId,
+ outcome: acp::ToolCallConfirmationOutcome,
+ cx: &mut Context,
+ ) {
+ let Some((ix, call)) = self.tool_call_mut(id) else {
+ return;
+ };
+
+ let new_status = if outcome == acp::ToolCallConfirmationOutcome::Reject {
+ ToolCallStatus::Rejected
+ } else {
+ ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::Running,
+ }
+ };
+
+ let curr_status = mem::replace(&mut call.status, new_status);
+
+ if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
+ respond_tx.send(outcome).log_err();
+ } else if cfg!(debug_assertions) {
+ panic!("tried to authorize an already authorized tool call");
+ }
+
+ cx.emit(AcpThreadEvent::EntryUpdated(ix));
+ }
+
+ pub fn update_tool_call(
+ &mut self,
+ id: acp::ToolCallId,
+ new_status: acp::ToolCallStatus,
+ new_content: Option,
+ cx: &mut Context,
+ ) -> Result<()> {
+ let language_registry = self.project.read(cx).languages().clone();
+ let (ix, call) = self.tool_call_mut(id).context("Entry not found")?;
+
+ call.content = new_content
+ .map(|new_content| ToolCallContent::from_acp(new_content, language_registry, cx));
+
+ match &mut call.status {
+ ToolCallStatus::Allowed { status } => {
+ *status = new_status;
+ }
+ ToolCallStatus::WaitingForConfirmation { .. } => {
+ anyhow::bail!("Tool call hasn't been authorized yet")
+ }
+ ToolCallStatus::Rejected => {
+ anyhow::bail!("Tool call was rejected and therefore can't be updated")
+ }
+ ToolCallStatus::Canceled => {
+ call.status = ToolCallStatus::Allowed { status: new_status };
+ }
+ }
+
+ cx.emit(AcpThreadEvent::EntryUpdated(ix));
+ Ok(())
+ }
+
+ fn tool_call_mut(&mut self, id: acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
+ let entry = self.entries.get_mut(id.0 as usize);
+ debug_assert!(
+ entry.is_some(),
+ "We shouldn't give out ids to entries that don't exist"
+ );
+ match entry {
+ Some(AgentThreadEntry::ToolCall(call)) if call.id == id => Some((id.0 as usize, call)),
+ _ => {
+ if cfg!(debug_assertions) {
+ panic!("entry is not a tool call");
+ }
+ None
+ }
+ }
+ }
+
+ /// Returns true if the last turn is awaiting tool authorization
+ pub fn waiting_for_tool_confirmation(&self) -> bool {
+ for entry in self.entries.iter().rev() {
+ match &entry {
+ AgentThreadEntry::ToolCall(call) => match call.status {
+ ToolCallStatus::WaitingForConfirmation { .. } => return true,
+ ToolCallStatus::Allowed { .. }
+ | ToolCallStatus::Rejected
+ | ToolCallStatus::Canceled => continue,
+ },
+ AgentThreadEntry::UserMessage(_) | AgentThreadEntry::AssistantMessage(_) => {
+ // Reached the beginning of the turn
+ return false;
+ }
+ }
+ }
+ false
+ }
+
+ pub fn initialize(&self) -> impl use<> + Future