Detailed changes
@@ -6,6 +6,7 @@ version = 4
name = "acp_thread"
version = "0.1.0"
dependencies = [
+ "agent-client-protocol",
"agentic-coding-protocol",
"anyhow",
"assistant_tool",
@@ -135,11 +136,23 @@ dependencies = [
"zstd",
]
+[[package]]
+name = "agent-client-protocol"
+version = "0.0.10"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7fb7f39671e02f8a1aeb625652feae40b6fc2597baaa97e028a98863477aecbd"
+dependencies = [
+ "schemars",
+ "serde",
+ "serde_json",
+]
+
[[package]]
name = "agent_servers"
version = "0.1.0"
dependencies = [
"acp_thread",
+ "agent-client-protocol",
"agentic-coding-protocol",
"anyhow",
"collections",
@@ -195,9 +208,9 @@ version = "0.1.0"
dependencies = [
"acp_thread",
"agent",
+ "agent-client-protocol",
"agent_servers",
"agent_settings",
- "agentic-coding-protocol",
"ai_onboarding",
"anyhow",
"assistant_context",
@@ -413,6 +413,7 @@ zlog_settings = { path = "crates/zlog_settings" }
#
agentic-coding-protocol = "0.0.10"
+agent-client-protocol = "0.0.10"
aho-corasick = "1.1"
alacritty_terminal = { git = "https://github.com/zed-industries/alacritty.git", branch = "add-hush-login-flag" }
any_vec = "0.14"
@@ -16,6 +16,7 @@ doctest = false
test-support = ["gpui/test-support", "project/test-support"]
[dependencies]
+agent-client-protocol.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
assistant_tool.workspace = true
@@ -1,17 +1,15 @@
mod connection;
+mod old_acp_support;
pub use connection::*;
+pub use old_acp_support::*;
-pub use acp::ToolCallId;
-use agentic_coding_protocol::{
- self as acp, AgentRequest, ProtocolVersion, ToolCallConfirmationOutcome, ToolCallLocation,
- UserMessageChunk,
-};
+use agent_client_protocol as acp;
use anyhow::{Context as _, Result};
use assistant_tool::ActionLog;
use buffer_diff::BufferDiff;
use editor::{Bias, MultiBuffer, PathKey};
use futures::{FutureExt, channel::oneshot, future::BoxFuture};
-use gpui::{AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity};
+use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task};
use itertools::Itertools;
use language::{
Anchor, Buffer, BufferSnapshot, Capability, LanguageRegistry, OffsetRangeExt as _, Point,
@@ -21,46 +19,37 @@ use markdown::Markdown;
use project::{AgentLocation, Project};
use std::collections::HashMap;
use std::error::Error;
-use std::fmt::{Formatter, Write};
+use std::fmt::Formatter;
+use std::rc::Rc;
use std::{
fmt::Display,
mem,
path::{Path, PathBuf},
sync::Arc,
};
-use ui::{App, IconName};
+use ui::App;
use util::ResultExt;
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Debug)]
pub struct UserMessage {
- pub content: Entity<Markdown>,
+ pub content: ContentBlock,
}
impl UserMessage {
pub fn from_acp(
- message: &acp::SendUserMessageParams,
+ message: impl IntoIterator<Item = acp::ContentBlock>,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) -> Self {
- let mut md_source = String::new();
-
- for chunk in &message.chunks {
- match chunk {
- UserMessageChunk::Text { text } => md_source.push_str(&text),
- 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)),
+ let mut content = ContentBlock::Empty;
+ for chunk in message {
+ content.append(chunk, &language_registry, cx)
}
+ Self { content: content }
}
fn to_markdown(&self, cx: &App) -> String {
- format!("## User\n\n{}\n\n", self.content.read(cx).source())
+ format!("## User\n\n{}\n\n", self.content.to_markdown(cx))
}
}
@@ -96,7 +85,7 @@ impl Display for MentionPath<'_> {
}
}
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Debug, PartialEq)]
pub struct AssistantMessage {
pub chunks: Vec<AssistantMessageChunk>,
}
@@ -113,42 +102,24 @@ impl AssistantMessage {
}
}
-#[derive(Clone, Debug, Eq, PartialEq)]
+#[derive(Debug, PartialEq)]
pub enum AssistantMessageChunk {
- Text { chunk: Entity<Markdown> },
- Thought { chunk: Entity<Markdown> },
+ Message { block: ContentBlock },
+ Thought { block: ContentBlock },
}
impl AssistantMessageChunk {
- pub fn from_acp(
- chunk: acp::AssistantMessageChunk,
- language_registry: Arc<LanguageRegistry>,
- cx: &mut App,
- ) -> Self {
- match chunk {
- acp::AssistantMessageChunk::Text { text } => Self::Text {
- chunk: cx.new(|cx| Markdown::new(text.into(), Some(language_registry), None, cx)),
- },
- acp::AssistantMessageChunk::Thought { thought } => Self::Thought {
- chunk: cx
- .new(|cx| Markdown::new(thought.into(), Some(language_registry), None, cx)),
- },
- }
- }
-
- pub fn from_str(chunk: &str, language_registry: Arc<LanguageRegistry>, cx: &mut App) -> Self {
- Self::Text {
- chunk: cx.new(|cx| {
- Markdown::new(chunk.to_owned().into(), Some(language_registry), None, cx)
- }),
+ pub fn from_str(chunk: &str, language_registry: &Arc<LanguageRegistry>, cx: &mut App) -> Self {
+ Self::Message {
+ block: ContentBlock::new(chunk.into(), language_registry, cx),
}
}
fn to_markdown(&self, cx: &App) -> String {
match self {
- Self::Text { chunk } => chunk.read(cx).source().to_string(),
- Self::Thought { chunk } => {
- format!("<thinking>\n{}\n</thinking>", chunk.read(cx).source())
+ Self::Message { block } => block.to_markdown(cx).to_string(),
+ Self::Thought { block } => {
+ format!("<thinking>\n{}\n</thinking>", block.to_markdown(cx))
}
}
}
@@ -166,19 +137,15 @@ impl AgentThreadEntry {
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),
+ Self::ToolCall(tool_call) => tool_call.to_markdown(cx),
}
}
- pub fn diff(&self) -> Option<&Diff> {
- if let AgentThreadEntry::ToolCall(ToolCall {
- content: Some(ToolCallContent::Diff { diff }),
- ..
- }) = self
- {
- Some(&diff)
+ pub fn diffs(&self) -> impl Iterator<Item = &Diff> {
+ if let AgentThreadEntry::ToolCall(call) = self {
+ itertools::Either::Left(call.diffs())
} else {
- None
+ itertools::Either::Right(std::iter::empty())
}
}
@@ -195,20 +162,54 @@ impl AgentThreadEntry {
pub struct ToolCall {
pub id: acp::ToolCallId,
pub label: Entity<Markdown>,
- pub icon: IconName,
- pub content: Option<ToolCallContent>,
+ pub kind: acp::ToolKind,
+ pub content: Vec<ToolCallContent>,
pub status: ToolCallStatus,
pub locations: Vec<acp::ToolCallLocation>,
}
impl ToolCall {
+ fn from_acp(
+ tool_call: acp::ToolCall,
+ status: ToolCallStatus,
+ language_registry: Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ Self {
+ id: tool_call.id,
+ label: cx.new(|cx| {
+ Markdown::new(
+ tool_call.label.into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ }),
+ kind: tool_call.kind,
+ content: tool_call
+ .content
+ .into_iter()
+ .map(|content| ToolCallContent::from_acp(content, language_registry.clone(), cx))
+ .collect(),
+ locations: tool_call.locations,
+ status,
+ }
+ }
+
+ pub fn diffs(&self) -> impl Iterator<Item = &Diff> {
+ self.content.iter().filter_map(|content| match content {
+ ToolCallContent::ContentBlock { .. } => None,
+ ToolCallContent::Diff { diff } => Some(diff),
+ })
+ }
+
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 {
+ for content in &self.content {
markdown.push_str(content.to_markdown(cx).as_str());
markdown.push_str("\n\n");
}
@@ -219,8 +220,8 @@ impl ToolCall {
#[derive(Debug)]
pub enum ToolCallStatus {
WaitingForConfirmation {
- confirmation: ToolCallConfirmation,
- respond_tx: oneshot::Sender<acp::ToolCallConfirmationOutcome>,
+ options: Vec<acp::PermissionOption>,
+ respond_tx: oneshot::Sender<acp::PermissionOptionId>,
},
Allowed {
status: acp::ToolCallStatus,
@@ -237,9 +238,9 @@ impl Display for ToolCallStatus {
match self {
ToolCallStatus::WaitingForConfirmation { .. } => "Waiting for confirmation",
ToolCallStatus::Allowed { status } => match status {
- acp::ToolCallStatus::Running => "Running",
- acp::ToolCallStatus::Finished => "Finished",
- acp::ToolCallStatus::Error => "Error",
+ acp::ToolCallStatus::InProgress => "In Progress",
+ acp::ToolCallStatus::Completed => "Completed",
+ acp::ToolCallStatus::Failed => "Failed",
},
ToolCallStatus::Rejected => "Rejected",
ToolCallStatus::Canceled => "Canceled",
@@ -248,86 +249,92 @@ impl Display for ToolCallStatus {
}
}
-#[derive(Debug)]
-pub enum ToolCallConfirmation {
- Edit {
- description: Option<Entity<Markdown>>,
- },
- Execute {
- command: String,
- root_command: String,
- description: Option<Entity<Markdown>>,
- },
- Mcp {
- server_name: String,
- tool_name: String,
- tool_display_name: String,
- description: Option<Entity<Markdown>>,
- },
- Fetch {
- urls: Vec<SharedString>,
- description: Option<Entity<Markdown>>,
- },
- Other {
- description: Entity<Markdown>,
- },
+#[derive(Debug, PartialEq, Clone)]
+pub enum ContentBlock {
+ Empty,
+ Markdown { markdown: Entity<Markdown> },
}
-impl ToolCallConfirmation {
- pub fn from_acp(
- confirmation: acp::ToolCallConfirmation,
+impl ContentBlock {
+ pub fn new(
+ block: acp::ContentBlock,
+ language_registry: &Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) -> Self {
+ let mut this = Self::Empty;
+ this.append(block, language_registry, cx);
+ this
+ }
+
+ pub fn new_combined(
+ blocks: impl IntoIterator<Item = acp::ContentBlock>,
language_registry: Arc<LanguageRegistry>,
cx: &mut App,
) -> Self {
- let to_md = |description: String, cx: &mut App| -> Entity<Markdown> {
- cx.new(|cx| {
- Markdown::new(
- description.into(),
- Some(language_registry.clone()),
- None,
- cx,
- )
- })
+ let mut this = Self::Empty;
+ for block in blocks {
+ this.append(block, &language_registry, cx);
+ }
+ this
+ }
+
+ pub fn append(
+ &mut self,
+ block: acp::ContentBlock,
+ language_registry: &Arc<LanguageRegistry>,
+ cx: &mut App,
+ ) {
+ let new_content = match block {
+ acp::ContentBlock::Text(text_content) => text_content.text.clone(),
+ acp::ContentBlock::ResourceLink(resource_link) => {
+ if let Some(path) = resource_link.uri.strip_prefix("file://") {
+ format!("{}", MentionPath(path.as_ref()))
+ } else {
+ resource_link.uri.clone()
+ }
+ }
+ acp::ContentBlock::Image(_)
+ | acp::ContentBlock::Audio(_)
+ | acp::ContentBlock::Resource(_) => String::new(),
};
- 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),
- },
+ match self {
+ ContentBlock::Empty => {
+ *self = ContentBlock::Markdown {
+ markdown: cx.new(|cx| {
+ Markdown::new(
+ new_content.into(),
+ Some(language_registry.clone()),
+ None,
+ cx,
+ )
+ }),
+ };
+ }
+ ContentBlock::Markdown { markdown } => {
+ markdown.update(cx, |markdown, cx| markdown.append(&new_content, cx));
+ }
+ }
+ }
+
+ fn to_markdown<'a>(&'a self, cx: &'a App) -> &'a str {
+ match self {
+ ContentBlock::Empty => "",
+ ContentBlock::Markdown { markdown } => markdown.read(cx).source(),
+ }
+ }
+
+ pub fn markdown(&self) -> Option<&Entity<Markdown>> {
+ match self {
+ ContentBlock::Empty => None,
+ ContentBlock::Markdown { markdown } => Some(markdown),
}
}
}
#[derive(Debug)]
pub enum ToolCallContent {
- Markdown { markdown: Entity<Markdown> },
+ ContentBlock { content: ContentBlock },
Diff { diff: Diff },
}
@@ -338,8 +345,8 @@ impl ToolCallContent {
cx: &mut App,
) -> Self {
match content {
- acp::ToolCallContent::Markdown { markdown } => Self::Markdown {
- markdown: cx.new(|cx| Markdown::new_text(markdown.into(), cx)),
+ acp::ToolCallContent::ContentBlock { content } => Self::ContentBlock {
+ content: ContentBlock::new(content, &language_registry, cx),
},
acp::ToolCallContent::Diff { diff } => Self::Diff {
diff: Diff::from_acp(diff, language_registry, cx),
@@ -347,9 +354,9 @@ impl ToolCallContent {
}
}
- fn to_markdown(&self, cx: &App) -> String {
+ pub fn to_markdown(&self, cx: &App) -> String {
match self {
- Self::Markdown { markdown } => markdown.read(cx).source().to_string(),
+ Self::ContentBlock { content } => content.to_markdown(cx).to_string(),
Self::Diff { diff } => diff.to_markdown(cx),
}
}
@@ -520,8 +527,8 @@ pub struct AcpThread {
action_log: Entity<ActionLog>,
shared_buffers: HashMap<Entity<Buffer>, BufferSnapshot>,
send_task: Option<Task<()>>,
- connection: Arc<dyn AgentConnection>,
- child_status: Option<Task<Result<()>>>,
+ connection: Rc<dyn AgentConnection>,
+ session_id: acp::SessionId,
}
pub enum AcpThreadEvent {
@@ -563,10 +570,9 @@ impl Error for LoadError {}
impl AcpThread {
pub fn new(
- connection: impl AgentConnection + 'static,
- title: SharedString,
- child_status: Option<Task<Result<()>>>,
+ connection: Rc<dyn AgentConnection>,
project: Entity<Project>,
+ session_id: acp::SessionId,
cx: &mut Context<Self>,
) -> Self {
let action_log = cx.new(|_| ActionLog::new(project.clone()));
@@ -576,24 +582,11 @@ impl AcpThread {
shared_buffers: Default::default(),
entries: Default::default(),
plan: Default::default(),
- title,
+ title: connection.name().into(),
project,
send_task: None,
- connection: Arc::new(connection),
- child_status,
- }
- }
-
- /// Send a request to the agent and wait for a response.
- pub fn request<R: AgentRequest + 'static>(
- &self,
- params: R,
- ) -> impl use<R> + Future<Output = Result<R::Response>> {
- let params = params.into_any();
- let result = self.connection.request_any(params);
- async move {
- let result = result.await?;
- Ok(R::response_from_any(result)?)
+ connection,
+ session_id,
}
}
@@ -629,15 +622,7 @@ impl AcpThread {
for entry in self.entries.iter().rev() {
match entry {
AgentThreadEntry::UserMessage(_) => return false,
- AgentThreadEntry::ToolCall(ToolCall {
- status:
- ToolCallStatus::Allowed {
- status: acp::ToolCallStatus::Running,
- ..
- },
- content: Some(ToolCallContent::Diff { .. }),
- ..
- }) => return true,
+ AgentThreadEntry::ToolCall(call) if call.diffs().next().is_some() => return true,
AgentThreadEntry::ToolCall(_) | AgentThreadEntry::AssistantMessage(_) => {}
}
}
@@ -652,42 +637,37 @@ impl AcpThread {
pub fn push_assistant_chunk(
&mut self,
- chunk: acp::AssistantMessageChunk,
+ chunk: acp::ContentBlock,
+ is_thought: bool,
cx: &mut Context<Self>,
) {
+ let language_registry = self.project.read(cx).languages().clone();
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 { text: new_chunk },
- )
- | (
- Some(AssistantMessageChunk::Thought { chunk: old_chunk }),
- acp::AssistantMessageChunk::Thought { thought: new_chunk },
- ) => {
- old_chunk.update(cx, |old_chunk, cx| {
- old_chunk.append(&new_chunk, cx);
- });
+ match (chunks.last_mut(), is_thought) {
+ (Some(AssistantMessageChunk::Message { block }), false)
+ | (Some(AssistantMessageChunk::Thought { block }), true) => {
+ block.append(chunk, &language_registry, cx)
}
_ => {
- chunks.push(AssistantMessageChunk::from_acp(
- chunk,
- self.project.read(cx).languages().clone(),
- cx,
- ));
+ let block = ContentBlock::new(chunk, &language_registry, cx);
+ if is_thought {
+ chunks.push(AssistantMessageChunk::Thought { block })
+ } else {
+ chunks.push(AssistantMessageChunk::Message { block })
+ }
}
}
} else {
- let chunk = AssistantMessageChunk::from_acp(
- chunk,
- self.project.read(cx).languages().clone(),
- cx,
- );
+ let block = ContentBlock::new(chunk, &language_registry, cx);
+ let chunk = if is_thought {
+ AssistantMessageChunk::Thought { block }
+ } else {
+ AssistantMessageChunk::Message { block }
+ };
self.push_entry(
AgentThreadEntry::AssistantMessage(AssistantMessage {
@@ -698,122 +678,122 @@ impl AcpThread {
}
}
- pub fn request_new_tool_call(
- &mut self,
- tool_call: acp::RequestToolCallConfirmationParams,
- cx: &mut Context<Self>,
- ) -> ToolCallRequest {
- let (tx, rx) = oneshot::channel();
-
- let status = ToolCallStatus::WaitingForConfirmation {
- confirmation: ToolCallConfirmation::from_acp(
- tool_call.confirmation,
- self.project.read(cx).languages().clone(),
- cx,
- ),
- respond_tx: tx,
- };
-
- let id = self.insert_tool_call(tool_call.tool_call, status, cx);
- ToolCallRequest { id, outcome: rx }
- }
-
- pub fn request_tool_call_confirmation(
+ pub fn update_tool_call(
&mut self,
- tool_call_id: ToolCallId,
- confirmation: acp::ToolCallConfirmation,
+ id: acp::ToolCallId,
+ status: acp::ToolCallStatus,
+ content: Option<Vec<acp::ToolCallContent>>,
cx: &mut Context<Self>,
- ) -> Result<ToolCallRequest> {
- let project = self.project.read(cx).languages().clone();
- let Some((idx, call)) = self.tool_call_mut(tool_call_id) else {
- anyhow::bail!("Tool call not found");
- };
-
- let (tx, rx) = oneshot::channel();
+ ) -> Result<()> {
+ let languages = self.project.read(cx).languages().clone();
+ let (ix, current_call) = self.tool_call_mut(&id).context("Tool call not found")?;
- call.status = ToolCallStatus::WaitingForConfirmation {
- confirmation: ToolCallConfirmation::from_acp(confirmation, project, cx),
- respond_tx: tx,
- };
+ if let Some(content) = content {
+ current_call.content = content
+ .into_iter()
+ .map(|chunk| ToolCallContent::from_acp(chunk, languages.clone(), cx))
+ .collect();
+ }
+ current_call.status = ToolCallStatus::Allowed { status };
- cx.emit(AcpThreadEvent::EntryUpdated(idx));
+ cx.emit(AcpThreadEvent::EntryUpdated(ix));
- Ok(ToolCallRequest {
- id: tool_call_id,
- outcome: rx,
- })
+ Ok(())
}
- pub fn push_tool_call(
- &mut self,
- request: acp::PushToolCallParams,
- cx: &mut Context<Self>,
- ) -> acp::ToolCallId {
+ /// Updates a tool call if id matches an existing entry, otherwise inserts a new one.
+ pub fn upsert_tool_call(&mut self, tool_call: acp::ToolCall, cx: &mut Context<Self>) {
let status = ToolCallStatus::Allowed {
- status: acp::ToolCallStatus::Running,
+ status: tool_call.status,
};
-
- self.insert_tool_call(request, status, cx)
+ self.upsert_tool_call_inner(tool_call, status, cx)
}
- fn insert_tool_call(
+ pub fn upsert_tool_call_inner(
&mut self,
- tool_call: acp::PushToolCallParams,
+ tool_call: acp::ToolCall,
status: ToolCallStatus,
cx: &mut Context<Self>,
- ) -> acp::ToolCallId {
+ ) {
let language_registry = self.project.read(cx).languages().clone();
- let id = acp::ToolCallId(self.entries.len() as u64);
- let call = ToolCall {
- id,
- label: cx.new(|cx| {
- Markdown::new(
- tool_call.label.into(),
- Some(language_registry.clone()),
- None,
- cx,
- )
- }),
- icon: acp_icon_to_ui_icon(tool_call.icon),
- content: tool_call
- .content
- .map(|content| ToolCallContent::from_acp(content, language_registry, cx)),
- locations: tool_call.locations,
- status,
- };
+ let call = ToolCall::from_acp(tool_call, status, language_registry, cx);
let location = call.locations.last().cloned();
+
+ if let Some((ix, current_call)) = self.tool_call_mut(&call.id) {
+ *current_call = call;
+
+ cx.emit(AcpThreadEvent::EntryUpdated(ix));
+ } else {
+ self.push_entry(AgentThreadEntry::ToolCall(call), cx);
+ }
+
if let Some(location) = location {
self.set_project_location(location, cx)
}
+ }
+
+ fn tool_call_mut(&mut self, id: &acp::ToolCallId) -> Option<(usize, &mut ToolCall)> {
+ // The tool call we are looking for is typically the last one, or very close to the end.
+ // At the moment, it doesn't seem like a hashmap would be a good fit for this use case.
+ self.entries
+ .iter_mut()
+ .enumerate()
+ .rev()
+ .find_map(|(index, tool_call)| {
+ if let AgentThreadEntry::ToolCall(tool_call) = tool_call
+ && &tool_call.id == id
+ {
+ Some((index, tool_call))
+ } else {
+ None
+ }
+ })
+ }
+
+ pub fn request_tool_call_permission(
+ &mut self,
+ tool_call: acp::ToolCall,
+ options: Vec<acp::PermissionOption>,
+ cx: &mut Context<Self>,
+ ) -> oneshot::Receiver<acp::PermissionOptionId> {
+ let (tx, rx) = oneshot::channel();
- self.push_entry(AgentThreadEntry::ToolCall(call), cx);
+ let status = ToolCallStatus::WaitingForConfirmation {
+ options,
+ respond_tx: tx,
+ };
- id
+ self.upsert_tool_call_inner(tool_call, status, cx);
+ rx
}
pub fn authorize_tool_call(
&mut self,
id: acp::ToolCallId,
- outcome: acp::ToolCallConfirmationOutcome,
+ option_id: acp::PermissionOptionId,
+ option_kind: acp::PermissionOptionKind,
cx: &mut Context<Self>,
) {
- let Some((ix, call)) = self.tool_call_mut(id) else {
+ 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 new_status = match option_kind {
+ acp::PermissionOptionKind::RejectOnce | acp::PermissionOptionKind::RejectAlways => {
+ ToolCallStatus::Rejected
+ }
+ acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => {
+ ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::InProgress,
+ }
}
};
let curr_status = mem::replace(&mut call.status, new_status);
if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
- respond_tx.send(outcome).log_err();
+ respond_tx.send(option_id).log_err();
} else if cfg!(debug_assertions) {
panic!("tried to authorize an already authorized tool call");
}
@@ -821,70 +801,11 @@ impl AcpThread {
cx.emit(AcpThreadEvent::EntryUpdated(ix));
}
- pub fn update_tool_call(
- &mut self,
- id: acp::ToolCallId,
- new_status: acp::ToolCallStatus,
- new_content: Option<acp::ToolCallContent>,
- cx: &mut Context<Self>,
- ) -> Result<()> {
- let language_registry = self.project.read(cx).languages().clone();
- let (ix, call) = self.tool_call_mut(id).context("Entry not found")?;
-
- if let Some(new_content) = new_content {
- call.content = Some(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 };
- }
- }
-
- let location = call.locations.last().cloned();
- if let Some(location) = location {
- self.set_project_location(location, cx)
- }
-
- 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
- }
- }
- }
-
pub fn plan(&self) -> &Plan {
&self.plan
}
- pub fn update_plan(&mut self, request: acp::UpdatePlanParams, cx: &mut Context<Self>) {
+ pub fn update_plan(&mut self, request: acp::Plan, cx: &mut Context<Self>) {
self.plan = Plan {
entries: request
.entries
@@ -896,14 +817,14 @@ impl AcpThread {
cx.notify();
}
- pub fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) {
+ fn clear_completed_plan_entries(&mut self, cx: &mut Context<Self>) {
self.plan
.entries
.retain(|entry| !matches!(entry.status, acp::PlanEntryStatus::Completed));
cx.notify();
}
- pub fn set_project_location(&self, location: ToolCallLocation, cx: &mut Context<Self>) {
+ pub fn set_project_location(&self, location: acp::ToolCallLocation, cx: &mut Context<Self>) {
self.project.update(cx, |project, cx| {
let Some(path) = project.project_path_for_absolute_path(&location.path, cx) else {
return;
@@ -953,14 +874,8 @@ impl AcpThread {
false
}
- pub fn initialize(&self) -> impl use<> + Future<Output = Result<acp::InitializeResponse>> {
- self.request(acp::InitializeParams {
- protocol_version: ProtocolVersion::latest(),
- })
- }
-
- pub fn authenticate(&self) -> impl use<> + Future<Output = Result<()>> {
- self.request(acp::AuthenticateParams)
+ pub fn authenticate(&self, cx: &mut App) -> impl use<> + Future<Output = Result<()>> {
+ self.connection.authenticate(cx)
}
#[cfg(any(test, feature = "test-support"))]
@@ -968,39 +883,50 @@ impl AcpThread {
&mut self,
message: &str,
cx: &mut Context<Self>,
- ) -> BoxFuture<'static, Result<(), acp::Error>> {
+ ) -> BoxFuture<'static, Result<()>> {
self.send(
- acp::SendUserMessageParams {
- chunks: vec![acp::UserMessageChunk::Text {
- text: message.to_string(),
- }],
- },
+ vec![acp::ContentBlock::Text(acp::TextContent {
+ text: message.to_string(),
+ annotations: None,
+ })],
cx,
)
}
pub fn send(
&mut self,
- message: acp::SendUserMessageParams,
+ message: Vec<acp::ContentBlock>,
cx: &mut Context<Self>,
- ) -> BoxFuture<'static, Result<(), acp::Error>> {
+ ) -> BoxFuture<'static, Result<()>> {
+ let block = ContentBlock::new_combined(
+ message.clone(),
+ self.project.read(cx).languages().clone(),
+ cx,
+ );
self.push_entry(
- AgentThreadEntry::UserMessage(UserMessage::from_acp(
- &message,
- self.project.read(cx).languages().clone(),
- cx,
- )),
+ AgentThreadEntry::UserMessage(UserMessage { content: block }),
cx,
);
+ self.clear_completed_plan_entries(cx);
let (tx, rx) = oneshot::channel();
- let cancel = self.cancel(cx);
+ let cancel_task = self.cancel(cx);
self.send_task = Some(cx.spawn(async move |this, cx| {
async {
- cancel.await.log_err();
-
- let result = this.update(cx, |this, _| this.request(message))?.await;
+ cancel_task.await;
+
+ let result = this
+ .update(cx, |this, cx| {
+ this.connection.prompt(
+ acp::PromptToolArguments {
+ prompt: message,
+ session_id: this.session_id.clone(),
+ },
+ cx,
+ )
+ })?
+ .await;
tx.send(result).log_err();
this.update(cx, |this, _cx| this.send_task.take())?;
anyhow::Ok(())
@@ -1018,48 +944,38 @@ impl AcpThread {
.boxed()
}
- pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp::Error>> {
- if self.send_task.take().is_some() {
- let request = self.request(acp::CancelSendMessageParams);
- cx.spawn(async move |this, cx| {
- request.await?;
- this.update(cx, |this, _cx| {
- for entry in this.entries.iter_mut() {
- if let AgentThreadEntry::ToolCall(call) = entry {
- let cancel = matches!(
- call.status,
- ToolCallStatus::WaitingForConfirmation { .. }
- | ToolCallStatus::Allowed {
- status: acp::ToolCallStatus::Running
- }
- );
-
- if cancel {
- let curr_status =
- mem::replace(&mut call.status, ToolCallStatus::Canceled);
-
- if let ToolCallStatus::WaitingForConfirmation {
- respond_tx, ..
- } = curr_status
- {
- respond_tx
- .send(acp::ToolCallConfirmationOutcome::Cancel)
- .ok();
- }
- }
+ pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<()> {
+ let Some(send_task) = self.send_task.take() else {
+ return Task::ready(());
+ };
+
+ for entry in self.entries.iter_mut() {
+ if let AgentThreadEntry::ToolCall(call) = entry {
+ let cancel = matches!(
+ call.status,
+ ToolCallStatus::WaitingForConfirmation { .. }
+ | ToolCallStatus::Allowed {
+ status: acp::ToolCallStatus::InProgress
}
- }
- })?;
- Ok(())
- })
- } else {
- Task::ready(Ok(()))
+ );
+
+ if cancel {
+ call.status = ToolCallStatus::Canceled;
+ }
+ }
}
+
+ self.connection.cancel(&self.session_id, cx);
+
+ // Wait for the send task to complete
+ cx.foreground_executor().spawn(send_task)
}
pub fn read_text_file(
&self,
- request: acp::ReadTextFileParams,
+ path: PathBuf,
+ line: Option<u32>,
+ limit: Option<u32>,
reuse_shared_snapshot: bool,
cx: &mut Context<Self>,
) -> Task<Result<String>> {
@@ -1,20 +1,26 @@
-use agentic_coding_protocol as acp;
+use std::{path::Path, rc::Rc};
+
+use agent_client_protocol as acp;
use anyhow::Result;
-use futures::future::{FutureExt as _, LocalBoxFuture};
+use gpui::{AsyncApp, Entity, Task};
+use project::Project;
+use ui::App;
+
+use crate::AcpThread;
pub trait AgentConnection {
- fn request_any(
- &self,
- params: acp::AnyAgentRequest,
- ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>>;
-}
+ fn name(&self) -> &'static str;
+
+ fn new_thread(
+ self: Rc<Self>,
+ project: Entity<Project>,
+ cwd: &Path,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<Entity<AcpThread>>>;
+
+ fn authenticate(&self, cx: &mut App) -> Task<Result<()>>;
+
+ fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>>;
-impl AgentConnection for acp::AgentConnection {
- fn request_any(
- &self,
- params: acp::AnyAgentRequest,
- ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
- let task = self.request_any(params);
- async move { Ok(task.await?) }.boxed_local()
- }
+ fn cancel(&self, session_id: &acp::SessionId, cx: &mut App);
}
@@ -0,0 +1,461 @@
+// Translates old acp agents into the new schema
+use agent_client_protocol as acp;
+use agentic_coding_protocol::{self as acp_old, AgentRequest as _};
+use anyhow::{Context as _, Result};
+use futures::channel::oneshot;
+use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
+use project::Project;
+use std::{cell::RefCell, error::Error, fmt, path::Path, rc::Rc};
+use ui::App;
+
+use crate::{AcpThread, AcpThreadEvent, AgentConnection, ToolCallContent, ToolCallStatus};
+
+#[derive(Clone)]
+pub struct OldAcpClientDelegate {
+ thread: Rc<RefCell<WeakEntity<AcpThread>>>,
+ cx: AsyncApp,
+ next_tool_call_id: Rc<RefCell<u64>>,
+ // sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
+}
+
+impl OldAcpClientDelegate {
+ pub fn new(thread: Rc<RefCell<WeakEntity<AcpThread>>>, cx: AsyncApp) -> Self {
+ Self {
+ thread,
+ cx,
+ next_tool_call_id: Rc::new(RefCell::new(0)),
+ }
+ }
+}
+
+impl acp_old::Client for OldAcpClientDelegate {
+ async fn stream_assistant_message_chunk(
+ &self,
+ params: acp_old::StreamAssistantMessageChunkParams,
+ ) -> Result<(), acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread
+ .borrow()
+ .update(cx, |thread, cx| match params.chunk {
+ acp_old::AssistantMessageChunk::Text { text } => {
+ thread.push_assistant_chunk(text.into(), false, cx)
+ }
+ acp_old::AssistantMessageChunk::Thought { thought } => {
+ thread.push_assistant_chunk(thought.into(), true, cx)
+ }
+ })
+ .ok();
+ })?;
+
+ Ok(())
+ }
+
+ async fn request_tool_call_confirmation(
+ &self,
+ request: acp_old::RequestToolCallConfirmationParams,
+ ) -> Result<acp_old::RequestToolCallConfirmationResponse, acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ let old_acp_id = *self.next_tool_call_id.borrow() + 1;
+ self.next_tool_call_id.replace(old_acp_id);
+
+ let tool_call = into_new_tool_call(
+ acp::ToolCallId(old_acp_id.to_string().into()),
+ request.tool_call,
+ );
+
+ let mut options = match request.confirmation {
+ acp_old::ToolCallConfirmation::Edit { .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ "Always Allow Edits".to_string(),
+ )],
+ acp_old::ToolCallConfirmation::Execute { root_command, .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ format!("Always Allow {}", root_command),
+ )],
+ acp_old::ToolCallConfirmation::Mcp {
+ server_name,
+ tool_name,
+ ..
+ } => vec![
+ (
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
+ acp::PermissionOptionKind::AllowAlways,
+ format!("Always Allow {}", server_name),
+ ),
+ (
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllowTool,
+ acp::PermissionOptionKind::AllowAlways,
+ format!("Always Allow {}", tool_name),
+ ),
+ ],
+ acp_old::ToolCallConfirmation::Fetch { .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ "Always Allow".to_string(),
+ )],
+ acp_old::ToolCallConfirmation::Other { .. } => vec![(
+ acp_old::ToolCallConfirmationOutcome::AlwaysAllow,
+ acp::PermissionOptionKind::AllowAlways,
+ "Always Allow".to_string(),
+ )],
+ };
+
+ options.extend([
+ (
+ acp_old::ToolCallConfirmationOutcome::Allow,
+ acp::PermissionOptionKind::AllowOnce,
+ "Allow".to_string(),
+ ),
+ (
+ acp_old::ToolCallConfirmationOutcome::Reject,
+ acp::PermissionOptionKind::RejectOnce,
+ "Reject".to_string(),
+ ),
+ ]);
+
+ let mut outcomes = Vec::with_capacity(options.len());
+ let mut acp_options = Vec::with_capacity(options.len());
+
+ for (index, (outcome, kind, label)) in options.into_iter().enumerate() {
+ outcomes.push(outcome);
+ acp_options.push(acp::PermissionOption {
+ id: acp::PermissionOptionId(index.to_string().into()),
+ label,
+ kind,
+ })
+ }
+
+ let response = cx
+ .update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.request_tool_call_permission(tool_call, acp_options, cx)
+ })
+ })?
+ .context("Failed to update thread")?
+ .await;
+
+ let outcome = match response {
+ Ok(option_id) => outcomes[option_id.0.parse::<usize>().unwrap_or(0)],
+ Err(oneshot::Canceled) => acp_old::ToolCallConfirmationOutcome::Cancel,
+ };
+
+ Ok(acp_old::RequestToolCallConfirmationResponse {
+ id: acp_old::ToolCallId(old_acp_id),
+ outcome: outcome,
+ })
+ }
+
+ async fn push_tool_call(
+ &self,
+ request: acp_old::PushToolCallParams,
+ ) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ let old_acp_id = *self.next_tool_call_id.borrow() + 1;
+ self.next_tool_call_id.replace(old_acp_id);
+
+ cx.update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.upsert_tool_call(
+ into_new_tool_call(acp::ToolCallId(old_acp_id.to_string().into()), request),
+ cx,
+ )
+ })
+ })?
+ .context("Failed to update thread")?;
+
+ Ok(acp_old::PushToolCallResponse {
+ id: acp_old::ToolCallId(old_acp_id),
+ })
+ }
+
+ async fn update_tool_call(
+ &self,
+ request: acp_old::UpdateToolCallParams,
+ ) -> Result<(), acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ let languages = thread.project.read(cx).languages().clone();
+
+ if let Some((ix, tool_call)) = thread
+ .tool_call_mut(&acp::ToolCallId(request.tool_call_id.0.to_string().into()))
+ {
+ tool_call.status = ToolCallStatus::Allowed {
+ status: into_new_tool_call_status(request.status),
+ };
+ tool_call.content = request
+ .content
+ .into_iter()
+ .map(|content| {
+ ToolCallContent::from_acp(
+ into_new_tool_call_content(content),
+ languages.clone(),
+ cx,
+ )
+ })
+ .collect();
+
+ cx.emit(AcpThreadEvent::EntryUpdated(ix));
+ anyhow::Ok(())
+ } else {
+ anyhow::bail!("Tool call not found")
+ }
+ })
+ })?
+ .context("Failed to update thread")??;
+
+ Ok(())
+ }
+
+ async fn update_plan(&self, request: acp_old::UpdatePlanParams) -> Result<(), acp_old::Error> {
+ let cx = &mut self.cx.clone();
+
+ cx.update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.update_plan(
+ acp::Plan {
+ entries: request
+ .entries
+ .into_iter()
+ .map(into_new_plan_entry)
+ .collect(),
+ },
+ cx,
+ )
+ })
+ })?
+ .context("Failed to update thread")?;
+
+ Ok(())
+ }
+
+ async fn read_text_file(
+ &self,
+ acp_old::ReadTextFileParams { path, line, limit }: acp_old::ReadTextFileParams,
+ ) -> Result<acp_old::ReadTextFileResponse, acp_old::Error> {
+ let content = self
+ .cx
+ .update(|cx| {
+ self.thread.borrow().update(cx, |thread, cx| {
+ thread.read_text_file(path, line, limit, false, cx)
+ })
+ })?
+ .context("Failed to update thread")?
+ .await?;
+ Ok(acp_old::ReadTextFileResponse { content })
+ }
+
+ async fn write_text_file(
+ &self,
+ acp_old::WriteTextFileParams { path, content }: acp_old::WriteTextFileParams,
+ ) -> Result<(), acp_old::Error> {
+ self.cx
+ .update(|cx| {
+ self.thread
+ .borrow()
+ .update(cx, |thread, cx| thread.write_text_file(path, content, cx))
+ })?
+ .context("Failed to update thread")?
+ .await?;
+
+ Ok(())
+ }
+}
+
+fn into_new_tool_call(id: acp::ToolCallId, request: acp_old::PushToolCallParams) -> acp::ToolCall {
+ acp::ToolCall {
+ id: id,
+ label: request.label,
+ kind: acp_kind_from_old_icon(request.icon),
+ status: acp::ToolCallStatus::InProgress,
+ content: request
+ .content
+ .into_iter()
+ .map(into_new_tool_call_content)
+ .collect(),
+ locations: request
+ .locations
+ .into_iter()
+ .map(into_new_tool_call_location)
+ .collect(),
+ }
+}
+
+fn acp_kind_from_old_icon(icon: acp_old::Icon) -> acp::ToolKind {
+ match icon {
+ acp_old::Icon::FileSearch => acp::ToolKind::Search,
+ acp_old::Icon::Folder => acp::ToolKind::Search,
+ acp_old::Icon::Globe => acp::ToolKind::Search,
+ acp_old::Icon::Hammer => acp::ToolKind::Other,
+ acp_old::Icon::LightBulb => acp::ToolKind::Think,
+ acp_old::Icon::Pencil => acp::ToolKind::Edit,
+ acp_old::Icon::Regex => acp::ToolKind::Search,
+ acp_old::Icon::Terminal => acp::ToolKind::Execute,
+ }
+}
+
+fn into_new_tool_call_status(status: acp_old::ToolCallStatus) -> acp::ToolCallStatus {
+ match status {
+ acp_old::ToolCallStatus::Running => acp::ToolCallStatus::InProgress,
+ acp_old::ToolCallStatus::Finished => acp::ToolCallStatus::Completed,
+ acp_old::ToolCallStatus::Error => acp::ToolCallStatus::Failed,
+ }
+}
+
+fn into_new_tool_call_content(content: acp_old::ToolCallContent) -> acp::ToolCallContent {
+ match content {
+ acp_old::ToolCallContent::Markdown { markdown } => acp::ToolCallContent::ContentBlock {
+ content: acp::ContentBlock::Text(acp::TextContent {
+ annotations: None,
+ text: markdown,
+ }),
+ },
+ acp_old::ToolCallContent::Diff { diff } => acp::ToolCallContent::Diff {
+ diff: into_new_diff(diff),
+ },
+ }
+}
+
+fn into_new_diff(diff: acp_old::Diff) -> acp::Diff {
+ acp::Diff {
+ path: diff.path,
+ old_text: diff.old_text,
+ new_text: diff.new_text,
+ }
+}
+
+fn into_new_tool_call_location(location: acp_old::ToolCallLocation) -> acp::ToolCallLocation {
+ acp::ToolCallLocation {
+ path: location.path,
+ line: location.line,
+ }
+}
+
+fn into_new_plan_entry(entry: acp_old::PlanEntry) -> acp::PlanEntry {
+ acp::PlanEntry {
+ content: entry.content,
+ priority: into_new_plan_priority(entry.priority),
+ status: into_new_plan_status(entry.status),
+ }
+}
+
+fn into_new_plan_priority(priority: acp_old::PlanEntryPriority) -> acp::PlanEntryPriority {
+ match priority {
+ acp_old::PlanEntryPriority::Low => acp::PlanEntryPriority::Low,
+ acp_old::PlanEntryPriority::Medium => acp::PlanEntryPriority::Medium,
+ acp_old::PlanEntryPriority::High => acp::PlanEntryPriority::High,
+ }
+}
+
+fn into_new_plan_status(status: acp_old::PlanEntryStatus) -> acp::PlanEntryStatus {
+ match status {
+ acp_old::PlanEntryStatus::Pending => acp::PlanEntryStatus::Pending,
+ acp_old::PlanEntryStatus::InProgress => acp::PlanEntryStatus::InProgress,
+ acp_old::PlanEntryStatus::Completed => acp::PlanEntryStatus::Completed,
+ }
+}
+
+#[derive(Debug)]
+pub struct Unauthenticated;
+
+impl Error for Unauthenticated {}
+impl fmt::Display for Unauthenticated {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ write!(f, "Unauthenticated")
+ }
+}
+
+pub struct OldAcpAgentConnection {
+ pub name: &'static str,
+ pub connection: acp_old::AgentConnection,
+ pub child_status: Task<Result<()>>,
+}
+
+impl AgentConnection for OldAcpAgentConnection {
+ fn name(&self) -> &'static str {
+ self.name
+ }
+
+ fn new_thread(
+ self: Rc<Self>,
+ project: Entity<Project>,
+ _cwd: &Path,
+ cx: &mut AsyncApp,
+ ) -> Task<Result<Entity<AcpThread>>> {
+ let task = self.connection.request_any(
+ acp_old::InitializeParams {
+ protocol_version: acp_old::ProtocolVersion::latest(),
+ }
+ .into_any(),
+ );
+ cx.spawn(async move |cx| {
+ let result = task.await?;
+ let result = acp_old::InitializeParams::response_from_any(result)?;
+
+ if !result.is_authenticated {
+ anyhow::bail!(Unauthenticated)
+ }
+
+ cx.update(|cx| {
+ let thread = cx.new(|cx| {
+ let session_id = acp::SessionId("acp-old-no-id".into());
+ AcpThread::new(self.clone(), project, session_id, cx)
+ });
+ thread
+ })
+ })
+ }
+
+ fn authenticate(&self, cx: &mut App) -> Task<Result<()>> {
+ let task = self
+ .connection
+ .request_any(acp_old::AuthenticateParams.into_any());
+ cx.foreground_executor().spawn(async move {
+ task.await?;
+ Ok(())
+ })
+ }
+
+ fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
+ let chunks = params
+ .prompt
+ .into_iter()
+ .filter_map(|block| match block {
+ acp::ContentBlock::Text(text) => {
+ Some(acp_old::UserMessageChunk::Text { text: text.text })
+ }
+ acp::ContentBlock::ResourceLink(link) => Some(acp_old::UserMessageChunk::Path {
+ path: link.uri.into(),
+ }),
+ _ => None,
+ })
+ .collect();
+
+ let task = self
+ .connection
+ .request_any(acp_old::SendUserMessageParams { chunks }.into_any());
+ cx.foreground_executor().spawn(async move {
+ task.await?;
+ anyhow::Ok(())
+ })
+ }
+
+ fn cancel(&self, _session_id: &acp::SessionId, cx: &mut App) {
+ let task = self
+ .connection
+ .request_any(acp_old::CancelSendMessageParams.into_any());
+ cx.foreground_executor()
+ .spawn(async move {
+ task.await?;
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx)
+ }
+}
@@ -18,6 +18,7 @@ doctest = false
[dependencies]
acp_thread.workspace = true
+agent-client-protocol.workspace = true
agentic-coding-protocol.workspace = true
anyhow.workspace = true
collections.workspace = true
@@ -1,7 +1,6 @@
mod claude;
mod gemini;
mod settings;
-mod stdio_agent_server;
#[cfg(test)]
mod e2e_tests;
@@ -9,9 +8,8 @@ mod e2e_tests;
pub use claude::*;
pub use gemini::*;
pub use settings::*;
-pub use stdio_agent_server::*;
-use acp_thread::AcpThread;
+use acp_thread::AgentConnection;
use anyhow::Result;
use collections::HashMap;
use gpui::{App, AsyncApp, Entity, SharedString, Task};
@@ -20,6 +18,7 @@ use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
use std::{
path::{Path, PathBuf},
+ rc::Rc,
sync::Arc,
};
use util::ResultExt as _;
@@ -33,14 +32,14 @@ pub trait AgentServer: Send {
fn name(&self) -> &'static str;
fn empty_state_headline(&self) -> &'static str;
fn empty_state_message(&self) -> &'static str;
- fn supports_always_allow(&self) -> bool;
- fn new_thread(
+ fn connect(
&self,
+ // these will go away when old_acp is fully removed
root_dir: &Path,
project: &Entity<Project>,
cx: &mut App,
- ) -> Task<Result<Entity<AcpThread>>>;
+ ) -> Task<Result<Rc<dyn AgentConnection>>>;
}
impl std::fmt::Debug for AgentServerCommand {
@@ -1,5 +1,5 @@
mod mcp_server;
-mod tools;
+pub mod tools;
use collections::HashMap;
use project::Project;
@@ -12,28 +12,24 @@ use std::pin::pin;
use std::rc::Rc;
use uuid::Uuid;
-use agentic_coding_protocol::{
- self as acp, AnyAgentRequest, AnyAgentResult, Client, ProtocolVersion,
- StreamAssistantMessageChunkParams, ToolCallContent, UpdateToolCallParams,
-};
+use agent_client_protocol as acp;
use anyhow::{Result, anyhow};
use futures::channel::oneshot;
-use futures::future::LocalBoxFuture;
-use futures::{AsyncBufReadExt, AsyncWriteExt, SinkExt};
+use futures::{AsyncBufReadExt, AsyncWriteExt};
use futures::{
AsyncRead, AsyncWrite, FutureExt, StreamExt,
channel::mpsc::{self, UnboundedReceiver, UnboundedSender},
io::BufReader,
select_biased,
};
-use gpui::{App, AppContext, Entity, Task};
+use gpui::{App, AppContext, AsyncApp, Entity, Task, WeakEntity};
use serde::{Deserialize, Serialize};
use util::ResultExt;
-use crate::claude::mcp_server::ClaudeMcpServer;
+use crate::claude::mcp_server::{ClaudeZedMcpServer, McpConfig};
use crate::claude::tools::ClaudeTool;
use crate::{AgentServer, AgentServerCommand, AllAgentServersSettings};
-use acp_thread::{AcpClientDelegate, AcpThread, AgentConnection};
+use acp_thread::{AcpThread, AgentConnection};
#[derive(Clone)]
pub struct ClaudeCode;
@@ -55,29 +51,57 @@ impl AgentServer for ClaudeCode {
ui::IconName::AiClaude
}
- fn supports_always_allow(&self) -> bool {
- false
+ fn connect(
+ &self,
+ _root_dir: &Path,
+ _project: &Entity<Project>,
+ _cx: &mut App,
+ ) -> Task<Result<Rc<dyn AgentConnection>>> {
+ let connection = ClaudeAgentConnection {
+ sessions: Default::default(),
+ };
+
+ Task::ready(Ok(Rc::new(connection) as _))
+ }
+}
+
+#[cfg(unix)]
+fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
+ let pid = nix::unistd::Pid::from_raw(pid);
+
+ nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
+ .map_err(|e| anyhow!("Failed to interrupt process: {}", e))
+}
+
+#[cfg(windows)]
+fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
+ panic!("Cancel not implemented on Windows")
+}
+
+struct ClaudeAgentConnection {
+ sessions: Rc<RefCell<HashMap<acp::SessionId, ClaudeAgentSession>>>,
+}
+
+impl AgentConnection for ClaudeAgentConnection {
+ fn name(&self) -> &'static str {
+ ClaudeCode.name()
}
fn new_thread(
- &self,
- root_dir: &Path,
- project: &Entity<Project>,
- cx: &mut App,
+ self: Rc<Self>,
+ project: Entity<Project>,
+ cwd: &Path,
+ cx: &mut AsyncApp,
) -> Task<Result<Entity<AcpThread>>> {
- let project = project.clone();
- let root_dir = root_dir.to_path_buf();
- let title = self.name().into();
+ let cwd = cwd.to_owned();
cx.spawn(async move |cx| {
- let (mut delegate_tx, delegate_rx) = watch::channel(None);
- let tool_id_map = Rc::new(RefCell::new(HashMap::default()));
-
- let mcp_server = ClaudeMcpServer::new(delegate_rx, tool_id_map.clone(), cx).await?;
+ let (mut thread_tx, thread_rx) = watch::channel(WeakEntity::new_invalid());
+ let permission_mcp_server = ClaudeZedMcpServer::new(thread_rx.clone(), cx).await?;
let mut mcp_servers = HashMap::default();
mcp_servers.insert(
mcp_server::SERVER_NAME.to_string(),
- mcp_server.server_config()?,
+ permission_mcp_server.server_config()?,
);
let mcp_config = McpConfig { mcp_servers };
@@ -104,177 +128,180 @@ impl AgentServer for ClaudeCode {
let (outgoing_tx, outgoing_rx) = mpsc::unbounded();
let (cancel_tx, mut cancel_rx) = mpsc::unbounded::<oneshot::Sender<Result<()>>>();
- let session_id = Uuid::new_v4();
+ let session_id = acp::SessionId(Uuid::new_v4().to_string().into());
log::trace!("Starting session with id: {}", session_id);
- cx.background_spawn(async move {
- let mut outgoing_rx = Some(outgoing_rx);
- let mut mode = ClaudeSessionMode::Start;
-
- loop {
- let mut child =
- spawn_claude(&command, mode, session_id, &mcp_config_path, &root_dir)
- .await?;
- mode = ClaudeSessionMode::Resume;
-
- let pid = child.id();
- log::trace!("Spawned (pid: {})", pid);
-
- let mut io_fut = pin!(
- ClaudeAgentConnection::handle_io(
- outgoing_rx.take().unwrap(),
- incoming_message_tx.clone(),
- child.stdin.take().unwrap(),
- child.stdout.take().unwrap(),
+ cx.background_spawn({
+ let session_id = session_id.clone();
+ async move {
+ let mut outgoing_rx = Some(outgoing_rx);
+ let mut mode = ClaudeSessionMode::Start;
+
+ loop {
+ let mut child = spawn_claude(
+ &command,
+ mode,
+ session_id.clone(),
+ &mcp_config_path,
+ &cwd,
)
- .fuse()
- );
-
- select_biased! {
- done_tx = cancel_rx.next() => {
- if let Some(done_tx) = done_tx {
- log::trace!("Interrupted (pid: {})", pid);
- let result = send_interrupt(pid as i32);
- outgoing_rx.replace(io_fut.await?);
- done_tx.send(result).log_err();
- continue;
+ .await?;
+ mode = ClaudeSessionMode::Resume;
+
+ let pid = child.id();
+ log::trace!("Spawned (pid: {})", pid);
+
+ let mut io_fut = pin!(
+ ClaudeAgentSession::handle_io(
+ outgoing_rx.take().unwrap(),
+ incoming_message_tx.clone(),
+ child.stdin.take().unwrap(),
+ child.stdout.take().unwrap(),
+ )
+ .fuse()
+ );
+
+ select_biased! {
+ done_tx = cancel_rx.next() => {
+ if let Some(done_tx) = done_tx {
+ log::trace!("Interrupted (pid: {})", pid);
+ let result = send_interrupt(pid as i32);
+ outgoing_rx.replace(io_fut.await?);
+ done_tx.send(result).log_err();
+ continue;
+ }
+ }
+ result = io_fut => {
+ result?;
}
}
- result = io_fut => {
- result?;
- }
+
+ log::trace!("Stopped (pid: {})", pid);
+ break;
}
- log::trace!("Stopped (pid: {})", pid);
- break;
+ drop(mcp_config_path);
+ anyhow::Ok(())
}
-
- drop(mcp_config_path);
- anyhow::Ok(())
})
.detach();
- cx.new(|cx| {
- let end_turn_tx = Rc::new(RefCell::new(None));
- let delegate = AcpClientDelegate::new(cx.entity().downgrade(), cx.to_async());
- delegate_tx.send(Some(delegate.clone())).log_err();
-
- let handler_task = cx.foreground_executor().spawn({
- let end_turn_tx = end_turn_tx.clone();
- let tool_id_map = tool_id_map.clone();
- let delegate = delegate.clone();
- async move {
- while let Some(message) = incoming_message_rx.next().await {
- ClaudeAgentConnection::handle_message(
- delegate.clone(),
- message,
- end_turn_tx.clone(),
- tool_id_map.clone(),
- )
- .await
- }
+ let end_turn_tx = Rc::new(RefCell::new(None));
+ let handler_task = cx.spawn({
+ let end_turn_tx = end_turn_tx.clone();
+ let thread_rx = thread_rx.clone();
+ async move |cx| {
+ while let Some(message) = incoming_message_rx.next().await {
+ ClaudeAgentSession::handle_message(
+ thread_rx.clone(),
+ message,
+ end_turn_tx.clone(),
+ cx,
+ )
+ .await
}
- });
-
- let mut connection = ClaudeAgentConnection {
- delegate,
- outgoing_tx,
- end_turn_tx,
- cancel_tx,
- session_id,
- _handler_task: handler_task,
- _mcp_server: None,
- };
+ }
+ });
- connection._mcp_server = Some(mcp_server);
- acp_thread::AcpThread::new(connection, title, None, project.clone(), cx)
- })
- })
- }
-}
+ let thread =
+ cx.new(|cx| AcpThread::new(self.clone(), project, session_id.clone(), cx))?;
-#[cfg(unix)]
-fn send_interrupt(pid: libc::pid_t) -> anyhow::Result<()> {
- let pid = nix::unistd::Pid::from_raw(pid);
+ thread_tx.send(thread.downgrade())?;
- nix::sys::signal::kill(pid, nix::sys::signal::SIGINT)
- .map_err(|e| anyhow!("Failed to interrupt process: {}", e))
-}
+ let session = ClaudeAgentSession {
+ outgoing_tx,
+ end_turn_tx,
+ cancel_tx,
+ _handler_task: handler_task,
+ _mcp_server: Some(permission_mcp_server),
+ };
-#[cfg(windows)]
-fn send_interrupt(_pid: i32) -> anyhow::Result<()> {
- panic!("Cancel not implemented on Windows")
-}
+ self.sessions.borrow_mut().insert(session_id, session);
-impl AgentConnection for ClaudeAgentConnection {
- /// Send a request to the agent and wait for a response.
- fn request_any(
- &self,
- params: AnyAgentRequest,
- ) -> LocalBoxFuture<'static, Result<acp::AnyAgentResult>> {
- let delegate = self.delegate.clone();
- let end_turn_tx = self.end_turn_tx.clone();
- let outgoing_tx = self.outgoing_tx.clone();
- let mut cancel_tx = self.cancel_tx.clone();
- let session_id = self.session_id;
- async move {
- match params {
- // todo: consider sending an empty request so we get the init response?
- AnyAgentRequest::InitializeParams(_) => Ok(AnyAgentResult::InitializeResponse(
- acp::InitializeResponse {
- is_authenticated: true,
- protocol_version: ProtocolVersion::latest(),
- },
- )),
- AnyAgentRequest::AuthenticateParams(_) => {
- Err(anyhow!("Authentication not supported"))
+ Ok(thread)
+ })
+ }
+
+ fn authenticate(&self, _cx: &mut App) -> Task<Result<()>> {
+ Task::ready(Err(anyhow!("Authentication not supported")))
+ }
+
+ fn prompt(&self, params: acp::PromptToolArguments, cx: &mut App) -> Task<Result<()>> {
+ let sessions = self.sessions.borrow();
+ let Some(session) = sessions.get(¶ms.session_id) else {
+ return Task::ready(Err(anyhow!(
+ "Attempted to send message to nonexistent session {}",
+ params.session_id
+ )));
+ };
+
+ let (tx, rx) = oneshot::channel();
+ session.end_turn_tx.borrow_mut().replace(tx);
+
+ let mut content = String::new();
+ for chunk in params.prompt {
+ match chunk {
+ acp::ContentBlock::Text(text_content) => {
+ content.push_str(&text_content.text);
}
- AnyAgentRequest::SendUserMessageParams(message) => {
- delegate.clear_completed_plan_entries().await?;
-
- let (tx, rx) = oneshot::channel();
- end_turn_tx.borrow_mut().replace(tx);
- let mut content = String::new();
- for chunk in message.chunks {
- match chunk {
- agentic_coding_protocol::UserMessageChunk::Text { text } => {
- content.push_str(&text)
- }
- agentic_coding_protocol::UserMessageChunk::Path { path } => {
- content.push_str(&format!("@{path:?}"))
- }
- }
- }
- outgoing_tx.unbounded_send(SdkMessage::User {
- message: Message {
- role: Role::User,
- content: Content::UntaggedText(content),
- id: None,
- model: None,
- stop_reason: None,
- stop_sequence: None,
- usage: None,
- },
- session_id: Some(session_id),
- })?;
- rx.await??;
- Ok(AnyAgentResult::SendUserMessageResponse(
- acp::SendUserMessageResponse,
- ))
+ acp::ContentBlock::ResourceLink(resource_link) => {
+ content.push_str(&format!("@{}", resource_link.uri));
}
- AnyAgentRequest::CancelSendMessageParams(_) => {
- let (done_tx, done_rx) = oneshot::channel();
- cancel_tx.send(done_tx).await?;
- done_rx.await??;
-
- Ok(AnyAgentResult::CancelSendMessageResponse(
- acp::CancelSendMessageResponse,
- ))
+ acp::ContentBlock::Audio(_)
+ | acp::ContentBlock::Image(_)
+ | acp::ContentBlock::Resource(_) => {
+ // TODO
}
}
}
- .boxed_local()
+
+ if let Err(err) = session.outgoing_tx.unbounded_send(SdkMessage::User {
+ message: Message {
+ role: Role::User,
+ content: Content::UntaggedText(content),
+ id: None,
+ model: None,
+ stop_reason: None,
+ stop_sequence: None,
+ usage: None,
+ },
+ session_id: Some(params.session_id.to_string()),
+ }) {
+ return Task::ready(Err(anyhow!(err)));
+ }
+
+ cx.foreground_executor().spawn(async move {
+ rx.await??;
+ Ok(())
+ })
+ }
+
+ fn cancel(&self, session_id: &acp::SessionId, cx: &mut App) {
+ let sessions = self.sessions.borrow();
+ let Some(session) = sessions.get(&session_id) else {
+ log::warn!("Attempted to cancel nonexistent session {}", session_id);
+ return;
+ };
+
+ let (done_tx, done_rx) = oneshot::channel();
+ if session
+ .cancel_tx
+ .unbounded_send(done_tx)
+ .log_err()
+ .is_some()
+ {
+ let end_turn_tx = session.end_turn_tx.clone();
+ cx.foreground_executor()
+ .spawn(async move {
+ done_rx.await??;
+ if let Some(end_turn_tx) = end_turn_tx.take() {
+ end_turn_tx.send(Ok(())).ok();
+ }
+ anyhow::Ok(())
+ })
+ .detach_and_log_err(cx);
+ }
}
}
@@ -287,7 +314,7 @@ enum ClaudeSessionMode {
async fn spawn_claude(
command: &AgentServerCommand,
mode: ClaudeSessionMode,
- session_id: Uuid,
+ session_id: acp::SessionId,
mcp_config_path: &Path,
root_dir: &Path,
) -> Result<Child> {
@@ -327,88 +354,103 @@ async fn spawn_claude(
Ok(child)
}
-struct ClaudeAgentConnection {
- delegate: AcpClientDelegate,
- session_id: Uuid,
+struct ClaudeAgentSession {
outgoing_tx: UnboundedSender<SdkMessage>,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
cancel_tx: UnboundedSender<oneshot::Sender<Result<()>>>,
- _mcp_server: Option<ClaudeMcpServer>,
+ _mcp_server: Option<ClaudeZedMcpServer>,
_handler_task: Task<()>,
}
-impl ClaudeAgentConnection {
+impl ClaudeAgentSession {
async fn handle_message(
- delegate: AcpClientDelegate,
+ mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
message: SdkMessage,
end_turn_tx: Rc<RefCell<Option<oneshot::Sender<Result<()>>>>>,
- tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
+ cx: &mut AsyncApp,
) {
match message {
- SdkMessage::Assistant { message, .. } | SdkMessage::User { message, .. } => {
+ SdkMessage::Assistant {
+ message,
+ session_id: _,
+ }
+ | SdkMessage::User {
+ message,
+ session_id: _,
+ } => {
+ let Some(thread) = thread_rx
+ .recv()
+ .await
+ .log_err()
+ .and_then(|entity| entity.upgrade())
+ else {
+ log::error!("Received an SDK message but thread is gone");
+ return;
+ };
+
for chunk in message.content.chunks() {
match chunk {
ContentChunk::Text { text } | ContentChunk::UntaggedText(text) => {
- delegate
- .stream_assistant_message_chunk(StreamAssistantMessageChunkParams {
- chunk: acp::AssistantMessageChunk::Text { text },
+ thread
+ .update(cx, |thread, cx| {
+ thread.push_assistant_chunk(text.into(), false, cx)
})
- .await
.log_err();
}
ContentChunk::ToolUse { id, name, input } => {
let claude_tool = ClaudeTool::infer(&name, input);
- if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
- delegate
- .update_plan(acp::UpdatePlanParams {
- entries: params.todos.into_iter().map(Into::into).collect(),
- })
- .await
- .log_err();
- } else if let Some(resp) = delegate
- .push_tool_call(claude_tool.as_acp())
- .await
- .log_err()
- {
- tool_id_map.borrow_mut().insert(id, resp.id);
- }
+ thread
+ .update(cx, |thread, cx| {
+ if let ClaudeTool::TodoWrite(Some(params)) = claude_tool {
+ thread.update_plan(
+ acp::Plan {
+ entries: params
+ .todos
+ .into_iter()
+ .map(Into::into)
+ .collect(),
+ },
+ cx,
+ )
+ } else {
+ thread.upsert_tool_call(
+ claude_tool.as_acp(acp::ToolCallId(id.into())),
+ cx,
+ );
+ }
+ })
+ .log_err();
}
ContentChunk::ToolResult {
content,
tool_use_id,
} => {
- let id = tool_id_map.borrow_mut().remove(&tool_use_id);
- if let Some(id) = id {
- let content = content.to_string();
- delegate
- .update_tool_call(UpdateToolCallParams {
- tool_call_id: id,
- status: acp::ToolCallStatus::Finished,
- // Don't unset existing content
- content: (!content.is_empty()).then_some(
- ToolCallContent::Markdown {
- // For now we only include text content
- markdown: content,
- },
- ),
- })
- .await
- .log_err();
- }
+ let content = content.to_string();
+ thread
+ .update(cx, |thread, cx| {
+ thread.update_tool_call(
+ acp::ToolCallId(tool_use_id.into()),
+ acp::ToolCallStatus::Completed,
+ (!content.is_empty()).then(|| vec![content.into()]),
+ cx,
+ )
+ })
+ .log_err();
}
ContentChunk::Image
| ContentChunk::Document
| ContentChunk::Thinking
| ContentChunk::RedactedThinking
| ContentChunk::WebSearchToolResult => {
- delegate
- .stream_assistant_message_chunk(StreamAssistantMessageChunkParams {
- chunk: acp::AssistantMessageChunk::Text {
- text: format!("Unsupported content: {:?}", chunk),
- },
+ thread
+ .update(cx, |thread, cx| {
+ thread.push_assistant_chunk(
+ format!("Unsupported content: {:?}", chunk).into(),
+ false,
+ cx,
+ )
})
- .await
.log_err();
}
}
@@ -592,14 +634,14 @@ enum SdkMessage {
Assistant {
message: Message, // from Anthropic SDK
#[serde(skip_serializing_if = "Option::is_none")]
- session_id: Option<Uuid>,
+ session_id: Option<String>,
},
// A user message
User {
message: Message, // from Anthropic SDK
#[serde(skip_serializing_if = "Option::is_none")]
- session_id: Option<Uuid>,
+ session_id: Option<String>,
},
// Emitted as the last message in a conversation
@@ -661,21 +703,6 @@ enum PermissionMode {
Plan,
}
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct McpConfig {
- mcp_servers: HashMap<String, McpServerConfig>,
-}
-
-#[derive(Serialize)]
-#[serde(rename_all = "camelCase")]
-struct McpServerConfig {
- command: String,
- args: Vec<String>,
- #[serde(skip_serializing_if = "Option::is_none")]
- env: Option<HashMap<String, String>>,
-}
-
#[cfg(test)]
pub(crate) mod tests {
use super::*;
@@ -1,29 +1,22 @@
-use std::{cell::RefCell, rc::Rc};
+use std::path::PathBuf;
-use acp_thread::AcpClientDelegate;
-use agentic_coding_protocol::{self as acp, Client, ReadTextFileParams, WriteTextFileParams};
+use acp_thread::AcpThread;
+use agent_client_protocol as acp;
use anyhow::{Context, Result};
use collections::HashMap;
-use context_server::{
- listener::McpServer,
- types::{
- CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse,
- ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations,
- ToolResponseContent, ToolsCapabilities, requests,
- },
+use context_server::types::{
+ CallToolParams, CallToolResponse, Implementation, InitializeParams, InitializeResponse,
+ ListToolsResponse, ProtocolVersion, ServerCapabilities, Tool, ToolAnnotations,
+ ToolResponseContent, ToolsCapabilities, requests,
};
-use gpui::{App, AsyncApp, Task};
+use gpui::{App, AsyncApp, Entity, Task, WeakEntity};
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
-use util::debug_panic;
-use crate::claude::{
- McpServerConfig,
- tools::{ClaudeTool, EditToolParams, ReadToolParams},
-};
+use crate::claude::tools::{ClaudeTool, EditToolParams, ReadToolParams};
-pub struct ClaudeMcpServer {
- server: McpServer,
+pub struct ClaudeZedMcpServer {
+ server: context_server::listener::McpServer,
}
pub const SERVER_NAME: &str = "zed";
@@ -52,17 +45,16 @@ enum PermissionToolBehavior {
Deny,
}
-impl ClaudeMcpServer {
+impl ClaudeZedMcpServer {
pub async fn new(
- delegate: watch::Receiver<Option<AcpClientDelegate>>,
- tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
+ thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
cx: &AsyncApp,
) -> Result<Self> {
- let mut mcp_server = McpServer::new(cx).await?;
+ let mut mcp_server = context_server::listener::McpServer::new(cx).await?;
mcp_server.handle_request::<requests::Initialize>(Self::handle_initialize);
mcp_server.handle_request::<requests::ListTools>(Self::handle_list_tools);
mcp_server.handle_request::<requests::CallTool>(move |request, cx| {
- Self::handle_call_tool(request, delegate.clone(), tool_id_map.clone(), cx)
+ Self::handle_call_tool(request, thread_rx.clone(), cx)
});
Ok(Self { server: mcp_server })
@@ -70,9 +62,7 @@ impl ClaudeMcpServer {
pub fn server_config(&self) -> Result<McpServerConfig> {
let zed_path = std::env::current_exe()
- .context("finding current executable path for use in mcp_server")?
- .to_string_lossy()
- .to_string();
+ .context("finding current executable path for use in mcp_server")?;
Ok(McpServerConfig {
command: zed_path,
@@ -152,22 +142,19 @@ impl ClaudeMcpServer {
fn handle_call_tool(
request: CallToolParams,
- mut delegate_watch: watch::Receiver<Option<AcpClientDelegate>>,
- tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
+ mut thread_rx: watch::Receiver<WeakEntity<AcpThread>>,
cx: &App,
) -> Task<Result<CallToolResponse>> {
cx.spawn(async move |cx| {
- let Some(delegate) = delegate_watch.recv().await? else {
- debug_panic!("Sent None delegate");
- anyhow::bail!("Server not available");
+ let Some(thread) = thread_rx.recv().await?.upgrade() else {
+ anyhow::bail!("Thread closed");
};
if request.name.as_str() == PERMISSION_TOOL {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
- let result =
- Self::handle_permissions_tool_call(input, delegate, tool_id_map, cx).await?;
+ let result = Self::handle_permissions_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content: vec![ToolResponseContent::Text {
text: serde_json::to_string(&result)?,
@@ -179,7 +166,7 @@ impl ClaudeMcpServer {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
- let content = Self::handle_read_tool_call(input, delegate, cx).await?;
+ let content = Self::handle_read_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content,
is_error: None,
@@ -189,7 +176,7 @@ impl ClaudeMcpServer {
let input =
serde_json::from_value(request.arguments.context("Arguments required")?)?;
- Self::handle_edit_tool_call(input, delegate, cx).await?;
+ Self::handle_edit_tool_call(input, thread, cx).await?;
Ok(CallToolResponse {
content: vec![],
is_error: None,
@@ -202,49 +189,46 @@ impl ClaudeMcpServer {
}
fn handle_read_tool_call(
- params: ReadToolParams,
- delegate: AcpClientDelegate,
+ ReadToolParams {
+ abs_path,
+ offset,
+ limit,
+ }: ReadToolParams,
+ thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<Vec<ToolResponseContent>>> {
- cx.foreground_executor().spawn(async move {
- let response = delegate
- .read_text_file(ReadTextFileParams {
- path: params.abs_path,
- line: params.offset,
- limit: params.limit,
- })
+ cx.spawn(async move |cx| {
+ let content = thread
+ .update(cx, |thread, cx| {
+ thread.read_text_file(abs_path, offset, limit, false, cx)
+ })?
.await?;
- Ok(vec![ToolResponseContent::Text {
- text: response.content,
- }])
+ Ok(vec![ToolResponseContent::Text { text: content }])
})
}
fn handle_edit_tool_call(
params: EditToolParams,
- delegate: AcpClientDelegate,
+ thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<()>> {
- cx.foreground_executor().spawn(async move {
- let response = delegate
- .read_text_file_reusing_snapshot(ReadTextFileParams {
- path: params.abs_path.clone(),
- line: None,
- limit: None,
- })
+ cx.spawn(async move |cx| {
+ let content = thread
+ .update(cx, |threads, cx| {
+ threads.read_text_file(params.abs_path.clone(), None, None, true, cx)
+ })?
.await?;
- let new_content = response.content.replace(¶ms.old_text, ¶ms.new_text);
- if new_content == response.content {
+ let new_content = content.replace(¶ms.old_text, ¶ms.new_text);
+ if new_content == content {
return Err(anyhow::anyhow!("The old_text was not found in the content"));
}
- delegate
- .write_text_file(WriteTextFileParams {
- path: params.abs_path,
- content: new_content,
- })
+ thread
+ .update(cx, |threads, cx| {
+ threads.write_text_file(params.abs_path, new_content, cx)
+ })?
.await?;
Ok(())
@@ -253,44 +237,65 @@ impl ClaudeMcpServer {
fn handle_permissions_tool_call(
params: PermissionToolParams,
- delegate: AcpClientDelegate,
- tool_id_map: Rc<RefCell<HashMap<String, acp::ToolCallId>>>,
+ thread: Entity<AcpThread>,
cx: &AsyncApp,
) -> Task<Result<PermissionToolResponse>> {
- cx.foreground_executor().spawn(async move {
+ cx.spawn(async move |cx| {
let claude_tool = ClaudeTool::infer(¶ms.tool_name, params.input.clone());
- let tool_call_id = match params.tool_use_id {
- Some(tool_use_id) => tool_id_map
- .borrow()
- .get(&tool_use_id)
- .cloned()
- .context("Tool call ID not found")?,
+ let tool_call_id =
+ acp::ToolCallId(params.tool_use_id.context("Tool ID required")?.into());
- None => delegate.push_tool_call(claude_tool.as_acp()).await?.id,
- };
+ let allow_option_id = acp::PermissionOptionId("allow".into());
+ let reject_option_id = acp::PermissionOptionId("reject".into());
- let outcome = delegate
- .request_existing_tool_call_confirmation(
- tool_call_id,
- claude_tool.confirmation(None),
- )
+ let chosen_option = thread
+ .update(cx, |thread, cx| {
+ thread.request_tool_call_permission(
+ claude_tool.as_acp(tool_call_id),
+ vec![
+ acp::PermissionOption {
+ id: allow_option_id.clone(),
+ label: "Allow".into(),
+ kind: acp::PermissionOptionKind::AllowOnce,
+ },
+ acp::PermissionOption {
+ id: reject_option_id,
+ label: "Reject".into(),
+ kind: acp::PermissionOptionKind::RejectOnce,
+ },
+ ],
+ cx,
+ )
+ })?
.await?;
- match outcome {
- acp::ToolCallConfirmationOutcome::Allow
- | acp::ToolCallConfirmationOutcome::AlwaysAllow
- | acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer
- | acp::ToolCallConfirmationOutcome::AlwaysAllowTool => Ok(PermissionToolResponse {
+ if chosen_option == allow_option_id {
+ Ok(PermissionToolResponse {
behavior: PermissionToolBehavior::Allow,
updated_input: params.input,
- }),
- acp::ToolCallConfirmationOutcome::Reject
- | acp::ToolCallConfirmationOutcome::Cancel => Ok(PermissionToolResponse {
+ })
+ } else {
+ Ok(PermissionToolResponse {
behavior: PermissionToolBehavior::Deny,
updated_input: params.input,
- }),
+ })
}
})
}
}
+
+#[derive(Serialize)]
+#[serde(rename_all = "camelCase")]
+pub struct McpConfig {
+ pub mcp_servers: HashMap<String, McpServerConfig>,
+}
+
+#[derive(Serialize, Clone)]
+#[serde(rename_all = "camelCase")]
+pub struct McpServerConfig {
+ pub command: PathBuf,
+ pub args: Vec<String>,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub env: Option<HashMap<String, String>>,
+}
@@ -1,6 +1,6 @@
use std::path::PathBuf;
-use agentic_coding_protocol::{self as acp, PushToolCallParams, ToolCallLocation};
+use agent_client_protocol as acp;
use itertools::Itertools;
use schemars::JsonSchema;
use serde::{Deserialize, Serialize};
@@ -115,51 +115,36 @@ impl ClaudeTool {
Self::Other { name, .. } => name.clone(),
}
}
-
- pub fn content(&self) -> Option<acp::ToolCallContent> {
+ pub fn content(&self) -> Vec<acp::ToolCallContent> {
match &self {
- Self::Other { input, .. } => Some(acp::ToolCallContent::Markdown {
- markdown: format!(
+ Self::Other { input, .. } => vec![
+ format!(
"```json\n{}```",
serde_json::to_string_pretty(&input).unwrap_or("{}".to_string())
- ),
- }),
- Self::Task(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.prompt.clone(),
- }),
- Self::NotebookRead(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.notebook_path.display().to_string(),
- }),
- Self::NotebookEdit(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.new_source.clone(),
- }),
- Self::Terminal(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: format!(
+ )
+ .into(),
+ ],
+ Self::Task(Some(params)) => vec![params.prompt.clone().into()],
+ Self::NotebookRead(Some(params)) => {
+ vec![params.notebook_path.display().to_string().into()]
+ }
+ Self::NotebookEdit(Some(params)) => vec![params.new_source.clone().into()],
+ Self::Terminal(Some(params)) => vec![
+ format!(
"`{}`\n\n{}",
params.command,
params.description.as_deref().unwrap_or_default()
- ),
- }),
- Self::ReadFile(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.abs_path.display().to_string(),
- }),
- Self::Ls(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.path.display().to_string(),
- }),
- Self::Glob(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.to_string(),
- }),
- Self::Grep(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: format!("`{params}`"),
- }),
- Self::WebFetch(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.prompt.clone(),
- }),
- Self::WebSearch(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.to_string(),
- }),
- Self::TodoWrite(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params
+ )
+ .into(),
+ ],
+ Self::ReadFile(Some(params)) => vec![params.abs_path.display().to_string().into()],
+ Self::Ls(Some(params)) => vec![params.path.display().to_string().into()],
+ Self::Glob(Some(params)) => vec![params.to_string().into()],
+ Self::Grep(Some(params)) => vec![format!("`{params}`").into()],
+ Self::WebFetch(Some(params)) => vec![params.prompt.clone().into()],
+ Self::WebSearch(Some(params)) => vec![params.to_string().into()],
+ Self::TodoWrite(Some(params)) => vec![
+ params
.todos
.iter()
.map(|todo| {
@@ -174,34 +159,39 @@ impl ClaudeTool {
todo.content
)
})
- .join("\n"),
- }),
- Self::ExitPlanMode(Some(params)) => Some(acp::ToolCallContent::Markdown {
- markdown: params.plan.clone(),
- }),
- Self::Edit(Some(params)) => Some(acp::ToolCallContent::Diff {
+ .join("\n")
+ .into(),
+ ],
+ Self::ExitPlanMode(Some(params)) => vec![params.plan.clone().into()],
+ Self::Edit(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.abs_path.clone(),
old_text: Some(params.old_text.clone()),
new_text: params.new_text.clone(),
},
- }),
- Self::Write(Some(params)) => Some(acp::ToolCallContent::Diff {
+ }],
+ Self::Write(Some(params)) => vec![acp::ToolCallContent::Diff {
diff: acp::Diff {
path: params.file_path.clone(),
old_text: None,
new_text: params.content.clone(),
},
- }),
+ }],
Self::MultiEdit(Some(params)) => {
// todo: show multiple edits in a multibuffer?
- params.edits.first().map(|edit| acp::ToolCallContent::Diff {
- diff: acp::Diff {
- path: params.file_path.clone(),
- old_text: Some(edit.old_string.clone()),
- new_text: edit.new_string.clone(),
- },
- })
+ params
+ .edits
+ .first()
+ .map(|edit| {
+ vec![acp::ToolCallContent::Diff {
+ diff: acp::Diff {
+ path: params.file_path.clone(),
+ old_text: Some(edit.old_string.clone()),
+ new_text: edit.new_string.clone(),
+ },
+ }]
+ })
+ .unwrap_or_default()
}
Self::Task(None)
| Self::NotebookRead(None)
@@ -217,181 +207,80 @@ impl ClaudeTool {
| Self::ExitPlanMode(None)
| Self::Edit(None)
| Self::Write(None)
- | Self::MultiEdit(None) => None,
+ | Self::MultiEdit(None) => vec![],
}
}
- pub fn icon(&self) -> acp::Icon {
+ pub fn kind(&self) -> acp::ToolKind {
match self {
- Self::Task(_) => acp::Icon::Hammer,
- Self::NotebookRead(_) => acp::Icon::FileSearch,
- Self::NotebookEdit(_) => acp::Icon::Pencil,
- Self::Edit(_) => acp::Icon::Pencil,
- Self::MultiEdit(_) => acp::Icon::Pencil,
- Self::Write(_) => acp::Icon::Pencil,
- Self::ReadFile(_) => acp::Icon::FileSearch,
- Self::Ls(_) => acp::Icon::Folder,
- Self::Glob(_) => acp::Icon::FileSearch,
- Self::Grep(_) => acp::Icon::Regex,
- Self::Terminal(_) => acp::Icon::Terminal,
- Self::WebSearch(_) => acp::Icon::Globe,
- Self::WebFetch(_) => acp::Icon::Globe,
- Self::TodoWrite(_) => acp::Icon::LightBulb,
- Self::ExitPlanMode(_) => acp::Icon::Hammer,
- Self::Other { .. } => acp::Icon::Hammer,
- }
- }
-
- pub fn confirmation(&self, description: Option<String>) -> acp::ToolCallConfirmation {
- match &self {
- Self::Edit(_) | Self::Write(_) | Self::NotebookEdit(_) | Self::MultiEdit(_) => {
- acp::ToolCallConfirmation::Edit { description }
- }
- Self::WebFetch(params) => acp::ToolCallConfirmation::Fetch {
- urls: params
- .as_ref()
- .map(|p| vec![p.url.clone()])
- .unwrap_or_default(),
- description,
- },
- Self::Terminal(Some(BashToolParams {
- description,
- command,
- ..
- })) => acp::ToolCallConfirmation::Execute {
- command: command.clone(),
- root_command: command.clone(),
- description: description.clone(),
- },
- Self::ExitPlanMode(Some(params)) => acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {}", params.plan)
- } else {
- params.plan.clone()
- },
- },
- Self::Task(Some(params)) => acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {}", params.description)
- } else {
- params.description.clone()
- },
- },
- Self::Ls(Some(LsToolParams { path, .. }))
- | Self::ReadFile(Some(ReadToolParams { abs_path: path, .. })) => {
- let path = path.display();
- acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {path}")
- } else {
- path.to_string()
- },
- }
- }
- Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
- let path = notebook_path.display();
- acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {path}")
- } else {
- path.to_string()
- },
- }
- }
- Self::Glob(Some(params)) => acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {params}")
- } else {
- params.to_string()
- },
- },
- Self::Grep(Some(params)) => acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {params}")
- } else {
- params.to_string()
- },
- },
- Self::WebSearch(Some(params)) => acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {params}")
- } else {
- params.to_string()
- },
- },
- Self::TodoWrite(Some(params)) => {
- let params = params.todos.iter().map(|todo| &todo.content).join(", ");
- acp::ToolCallConfirmation::Other {
- description: if let Some(description) = description {
- format!("{description} {params}")
- } else {
- params
- },
- }
- }
- Self::Terminal(None)
- | Self::Task(None)
- | Self::NotebookRead(None)
- | Self::ExitPlanMode(None)
- | Self::Ls(None)
- | Self::Glob(None)
- | Self::Grep(None)
- | Self::ReadFile(None)
- | Self::WebSearch(None)
- | Self::TodoWrite(None)
- | Self::Other { .. } => acp::ToolCallConfirmation::Other {
- description: description.unwrap_or("".to_string()),
- },
+ Self::Task(_) => acp::ToolKind::Think,
+ Self::NotebookRead(_) => acp::ToolKind::Read,
+ Self::NotebookEdit(_) => acp::ToolKind::Edit,
+ Self::Edit(_) => acp::ToolKind::Edit,
+ Self::MultiEdit(_) => acp::ToolKind::Edit,
+ Self::Write(_) => acp::ToolKind::Edit,
+ Self::ReadFile(_) => acp::ToolKind::Read,
+ Self::Ls(_) => acp::ToolKind::Search,
+ Self::Glob(_) => acp::ToolKind::Search,
+ Self::Grep(_) => acp::ToolKind::Search,
+ Self::Terminal(_) => acp::ToolKind::Execute,
+ Self::WebSearch(_) => acp::ToolKind::Search,
+ Self::WebFetch(_) => acp::ToolKind::Fetch,
+ Self::TodoWrite(_) => acp::ToolKind::Think,
+ Self::ExitPlanMode(_) => acp::ToolKind::Think,
+ Self::Other { .. } => acp::ToolKind::Other,
}
}
pub fn locations(&self) -> Vec<acp::ToolCallLocation> {
match &self {
- Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![ToolCallLocation {
+ Self::Edit(Some(EditToolParams { abs_path, .. })) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: None,
}],
Self::MultiEdit(Some(MultiEditToolParams { file_path, .. })) => {
- vec![ToolCallLocation {
+ vec![acp::ToolCallLocation {
+ path: file_path.clone(),
+ line: None,
+ }]
+ }
+ Self::Write(Some(WriteToolParams { file_path, .. })) => {
+ vec![acp::ToolCallLocation {
path: file_path.clone(),
line: None,
}]
}
- Self::Write(Some(WriteToolParams { file_path, .. })) => vec![ToolCallLocation {
- path: file_path.clone(),
- line: None,
- }],
Self::ReadFile(Some(ReadToolParams {
abs_path, offset, ..
- })) => vec![ToolCallLocation {
+ })) => vec![acp::ToolCallLocation {
path: abs_path.clone(),
line: *offset,
}],
Self::NotebookRead(Some(NotebookReadToolParams { notebook_path, .. })) => {
- vec![ToolCallLocation {
+ vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::NotebookEdit(Some(NotebookEditToolParams { notebook_path, .. })) => {
- vec![ToolCallLocation {
+ vec![acp::ToolCallLocation {
path: notebook_path.clone(),
line: None,
}]
}
Self::Glob(Some(GlobToolParams {
path: Some(path), ..
- })) => vec![ToolCallLocation {
+ })) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
- Self::Ls(Some(LsToolParams { path, .. })) => vec![ToolCallLocation {
+ Self::Ls(Some(LsToolParams { path, .. })) => vec![acp::ToolCallLocation {
path: path.clone(),
line: None,
}],
Self::Grep(Some(GrepToolParams {
path: Some(path), ..
- })) => vec![ToolCallLocation {
+ })) => vec![acp::ToolCallLocation {
path: PathBuf::from(path),
line: None,
}],
@@ -414,11 +303,13 @@ impl ClaudeTool {
}
}
- pub fn as_acp(&self) -> PushToolCallParams {
- PushToolCallParams {
+ pub fn as_acp(&self, id: acp::ToolCallId) -> acp::ToolCall {
+ acp::ToolCall {
+ id,
+ kind: self.kind(),
+ status: acp::ToolCallStatus::InProgress,
label: self.label(),
content: self.content(),
- icon: self.icon(),
locations: self.locations(),
}
}
@@ -1,10 +1,9 @@
use std::{path::Path, sync::Arc, time::Duration};
use crate::{AgentServer, AgentServerSettings, AllAgentServersSettings};
-use acp_thread::{
- AcpThread, AgentThreadEntry, ToolCall, ToolCallConfirmation, ToolCallContent, ToolCallStatus,
-};
-use agentic_coding_protocol as acp;
+use acp_thread::{AcpThread, AgentThreadEntry, ToolCall, ToolCallStatus};
+use agent_client_protocol as acp;
+
use futures::{FutureExt, StreamExt, channel::mpsc, select};
use gpui::{Entity, TestAppContext};
use indoc::indoc;
@@ -54,19 +53,25 @@ pub async fn test_path_mentions(server: impl AgentServer + 'static, cx: &mut Tes
thread
.update(cx, |thread, cx| {
thread.send(
- acp::SendUserMessageParams {
- chunks: vec![
- acp::UserMessageChunk::Text {
- text: "Read the file ".into(),
- },
- acp::UserMessageChunk::Path {
- path: Path::new("foo.rs").into(),
- },
- acp::UserMessageChunk::Text {
- text: " and tell me what the content of the println! is".into(),
- },
- ],
- },
+ vec![
+ acp::ContentBlock::Text(acp::TextContent {
+ text: "Read the file ".into(),
+ annotations: None,
+ }),
+ acp::ContentBlock::ResourceLink(acp::ResourceLink {
+ uri: "foo.rs".into(),
+ name: "foo.rs".into(),
+ annotations: None,
+ description: None,
+ mime_type: None,
+ size: None,
+ title: None,
+ }),
+ acp::ContentBlock::Text(acp::TextContent {
+ text: " and tell me what the content of the println! is".into(),
+ annotations: None,
+ }),
+ ],
cx,
)
})
@@ -161,11 +166,8 @@ pub async fn test_tool_call_with_confirmation(
let tool_call_id = thread.read_with(cx, |thread, _cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
- status:
- ToolCallStatus::WaitingForConfirmation {
- confirmation: ToolCallConfirmation::Execute { root_command, .. },
- ..
- },
+ content,
+ status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread
.entries()
@@ -176,13 +178,18 @@ pub async fn test_tool_call_with_confirmation(
panic!();
};
- assert!(root_command.contains("touch"));
+ assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
- *id
+ id.clone()
});
thread.update(cx, |thread, cx| {
- thread.authorize_tool_call(tool_call_id, acp::ToolCallConfirmationOutcome::Allow, cx);
+ thread.authorize_tool_call(
+ tool_call_id,
+ acp::PermissionOptionId("0".into()),
+ acp::PermissionOptionKind::AllowOnce,
+ cx,
+ );
assert!(thread.entries().iter().any(|entry| matches!(
entry,
@@ -197,7 +204,7 @@ pub async fn test_tool_call_with_confirmation(
thread.read_with(cx, |thread, cx| {
let AgentThreadEntry::ToolCall(ToolCall {
- content: Some(ToolCallContent::Markdown { markdown }),
+ content,
status: ToolCallStatus::Allowed { .. },
..
}) = thread
@@ -209,13 +216,10 @@ pub async fn test_tool_call_with_confirmation(
panic!();
};
- markdown.read_with(cx, |md, _cx| {
- assert!(
- md.source().contains("Hello"),
- r#"Expected '{}' to contain "Hello""#,
- md.source()
- );
- });
+ assert!(
+ content.iter().any(|c| c.to_markdown(cx).contains("Hello")),
+ "Expected content to contain 'Hello'"
+ );
});
}
@@ -249,26 +253,20 @@ pub async fn test_cancel(server: impl AgentServer + 'static, cx: &mut TestAppCon
thread.read_with(cx, |thread, _cx| {
let AgentThreadEntry::ToolCall(ToolCall {
id,
- status:
- ToolCallStatus::WaitingForConfirmation {
- confirmation: ToolCallConfirmation::Execute { root_command, .. },
- ..
- },
+ content,
+ status: ToolCallStatus::WaitingForConfirmation { .. },
..
}) = &thread.entries()[first_tool_call_ix]
else {
panic!("{:?}", thread.entries()[1]);
};
- assert!(root_command.contains("touch"));
+ assert!(content.iter().any(|c| c.to_markdown(_cx).contains("touch")));
- *id
+ id.clone()
});
- thread
- .update(cx, |thread, cx| thread.cancel(cx))
- .await
- .unwrap();
+ let _ = thread.update(cx, |thread, cx| thread.cancel(cx));
full_turn.await.unwrap();
thread.read_with(cx, |thread, _| {
let AgentThreadEntry::ToolCall(ToolCall {
@@ -369,15 +367,16 @@ pub async fn new_test_thread(
current_dir: impl AsRef<Path>,
cx: &mut TestAppContext,
) -> Entity<AcpThread> {
- let thread = cx
- .update(|cx| server.new_thread(current_dir.as_ref(), &project, cx))
+ let connection = cx
+ .update(|cx| server.connect(current_dir.as_ref(), &project, cx))
.await
.unwrap();
- thread
- .update(cx, |thread, _| thread.initialize())
+ let thread = connection
+ .new_thread(project.clone(), current_dir.as_ref(), &mut cx.to_async())
.await
.unwrap();
+
thread
}
@@ -1,9 +1,17 @@
-use crate::stdio_agent_server::StdioAgentServer;
-use crate::{AgentServerCommand, AgentServerVersion};
+use anyhow::anyhow;
+use std::cell::RefCell;
+use std::path::Path;
+use std::rc::Rc;
+use util::ResultExt as _;
+
+use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
+use acp_thread::{AgentConnection, LoadError, OldAcpAgentConnection, OldAcpClientDelegate};
+use agentic_coding_protocol as acp_old;
use anyhow::{Context as _, Result};
-use gpui::{AsyncApp, Entity};
+use gpui::{AppContext as _, AsyncApp, Entity, Task, WeakEntity};
use project::Project;
use settings::SettingsStore;
+use ui::App;
use crate::AllAgentServersSettings;
@@ -12,7 +20,7 @@ pub struct Gemini;
const ACP_ARG: &str = "--experimental-acp";
-impl StdioAgentServer for Gemini {
+impl AgentServer for Gemini {
fn name(&self) -> &'static str {
"Gemini"
}
@@ -25,14 +33,88 @@ impl StdioAgentServer for Gemini {
"Ask questions, edit files, run commands.\nBe specific for the best results."
}
- fn supports_always_allow(&self) -> bool {
- true
- }
-
fn logo(&self) -> ui::IconName {
ui::IconName::AiGemini
}
+ fn connect(
+ &self,
+ root_dir: &Path,
+ project: &Entity<Project>,
+ cx: &mut App,
+ ) -> Task<Result<Rc<dyn AgentConnection>>> {
+ let root_dir = root_dir.to_path_buf();
+ let project = project.clone();
+ let this = self.clone();
+ let name = self.name();
+
+ cx.spawn(async move |cx| {
+ let command = this.command(&project, cx).await?;
+
+ 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();
+
+ let foreground_executor = cx.foreground_executor().clone();
+
+ let thread_rc = Rc::new(RefCell::new(WeakEntity::new_invalid()));
+
+ let (connection, io_fut) = acp_old::AgentConnection::connect_to_agent(
+ OldAcpClientDelegate::new(thread_rc.clone(), cx.clone()),
+ 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 {
+ let result = match child.status().await {
+ Err(e) => Err(anyhow!(e)),
+ Ok(result) if result.success() => Ok(()),
+ Ok(result) => {
+ if let Some(AgentServerVersion::Unsupported {
+ error_message,
+ upgrade_message,
+ upgrade_command,
+ }) = this.version(&command).await.log_err()
+ {
+ Err(anyhow!(LoadError::Unsupported {
+ error_message,
+ upgrade_message,
+ upgrade_command
+ }))
+ } else {
+ Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
+ }
+ }
+ };
+ drop(io_task);
+ result
+ });
+
+ let connection: Rc<dyn AgentConnection> = Rc::new(OldAcpAgentConnection {
+ name,
+ connection,
+ child_status,
+ });
+
+ Ok(connection)
+ })
+ }
+}
+
+impl Gemini {
async fn command(
&self,
project: &Entity<Project>,
@@ -1,119 +0,0 @@
-use crate::{AgentServer, AgentServerCommand, AgentServerVersion};
-use acp_thread::{AcpClientDelegate, AcpThread, LoadError};
-use agentic_coding_protocol as acp;
-use anyhow::{Result, anyhow};
-use gpui::{App, AsyncApp, Entity, Task, prelude::*};
-use project::Project;
-use std::path::Path;
-use util::ResultExt;
-
-pub trait StdioAgentServer: Send + Clone {
- fn logo(&self) -> ui::IconName;
- fn name(&self) -> &'static str;
- fn empty_state_headline(&self) -> &'static str;
- fn empty_state_message(&self) -> &'static str;
- fn supports_always_allow(&self) -> bool;
-
- fn command(
- &self,
- project: &Entity<Project>,
- cx: &mut AsyncApp,
- ) -> impl Future<Output = Result<AgentServerCommand>>;
-
- fn version(
- &self,
- command: &AgentServerCommand,
- ) -> impl Future<Output = Result<AgentServerVersion>> + Send;
-}
-
-impl<T: StdioAgentServer + 'static> AgentServer for T {
- fn name(&self) -> &'static str {
- self.name()
- }
-
- fn empty_state_headline(&self) -> &'static str {
- self.empty_state_headline()
- }
-
- fn empty_state_message(&self) -> &'static str {
- self.empty_state_message()
- }
-
- fn logo(&self) -> ui::IconName {
- self.logo()
- }
-
- fn supports_always_allow(&self) -> bool {
- self.supports_always_allow()
- }
-
- fn new_thread(
- &self,
- root_dir: &Path,
- project: &Entity<Project>,
- cx: &mut App,
- ) -> Task<Result<Entity<AcpThread>>> {
- let root_dir = root_dir.to_path_buf();
- let project = project.clone();
- let this = self.clone();
- let title = self.name().into();
-
- cx.spawn(async move |cx| {
- let command = this.command(&project, cx).await?;
-
- 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 {
- let result = match child.status().await {
- Err(e) => Err(anyhow!(e)),
- Ok(result) if result.success() => Ok(()),
- Ok(result) => {
- if let Some(AgentServerVersion::Unsupported {
- error_message,
- upgrade_message,
- upgrade_command,
- }) = this.version(&command).await.log_err()
- {
- Err(anyhow!(LoadError::Unsupported {
- error_message,
- upgrade_message,
- upgrade_command
- }))
- } else {
- Err(anyhow!(LoadError::Exited(result.code().unwrap_or(-127))))
- }
- }
- };
- drop(io_task);
- result
- });
-
- AcpThread::new(connection, title, Some(child_status), project.clone(), cx)
- })
- })
- }
-}
@@ -17,10 +17,10 @@ test-support = ["gpui/test-support", "language/test-support"]
[dependencies]
acp_thread.workspace = true
+agent-client-protocol.workspace = true
agent.workspace = true
-agentic-coding-protocol.workspace = true
-agent_settings.workspace = true
agent_servers.workspace = true
+agent_settings.workspace = true
ai_onboarding.workspace = true
anyhow.workspace = true
assistant_context.workspace = true
@@ -1,4 +1,4 @@
-use acp_thread::Plan;
+use acp_thread::{AgentConnection, Plan};
use agent_servers::AgentServer;
use std::cell::RefCell;
use std::collections::BTreeMap;
@@ -7,7 +7,7 @@ use std::rc::Rc;
use std::sync::Arc;
use std::time::Duration;
-use agentic_coding_protocol::{self as acp};
+use agent_client_protocol as acp;
use assistant_tool::ActionLog;
use buffer_diff::BufferDiff;
use collections::{HashMap, HashSet};
@@ -16,7 +16,6 @@ use editor::{
EditorStyle, MinimapVisibility, MultiBuffer, PathKey,
};
use file_icons::FileIcons;
-use futures::channel::oneshot;
use gpui::{
Action, Animation, AnimationExt, App, BorderStyle, EdgesRefinement, Empty, Entity, EntityId,
FocusHandle, Focusable, Hsla, Length, ListOffset, ListState, SharedString, StyleRefinement,
@@ -39,8 +38,7 @@ use zed_actions::agent::{Chat, NextHistoryMessage, PreviousHistoryMessage};
use ::acp_thread::{
AcpThread, AcpThreadEvent, AgentThreadEntry, AssistantMessage, AssistantMessageChunk, Diff,
- LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallConfirmation, ToolCallContent,
- ToolCallId, ToolCallStatus,
+ LoadError, MentionPath, ThreadStatus, ToolCall, ToolCallContent, ToolCallStatus,
};
use crate::acp::completion_provider::{ContextPickerCompletionProvider, MentionSet};
@@ -64,12 +62,13 @@ pub struct AcpThreadView {
last_error: Option<Entity<Markdown>>,
list_state: ListState,
auth_task: Option<Task<()>>,
- expanded_tool_calls: HashSet<ToolCallId>,
+ expanded_tool_calls: HashSet<acp::ToolCallId>,
expanded_thinking_blocks: HashSet<(usize, usize)>,
edits_expanded: bool,
plan_expanded: bool,
editor_expanded: bool,
- message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
+ message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
+ _cancel_task: Option<Task<()>>,
}
enum ThreadState {
@@ -82,22 +81,16 @@ enum ThreadState {
},
LoadError(LoadError),
Unauthenticated {
- thread: Entity<AcpThread>,
+ connection: Rc<dyn AgentConnection>,
},
}
-struct AlwaysAllowOption {
- id: &'static str,
- label: SharedString,
- outcome: acp::ToolCallConfirmationOutcome,
-}
-
impl AcpThreadView {
pub fn new(
agent: Rc<dyn AgentServer>,
workspace: WeakEntity<Workspace>,
project: Entity<Project>,
- message_history: Rc<RefCell<MessageHistory<acp::SendUserMessageParams>>>,
+ message_history: Rc<RefCell<MessageHistory<Vec<acp::ContentBlock>>>>,
min_lines: usize,
max_lines: Option<usize>,
window: &mut Window,
@@ -191,6 +184,7 @@ impl AcpThreadView {
plan_expanded: false,
editor_expanded: false,
message_history,
+ _cancel_task: None,
}
}
@@ -208,9 +202,9 @@ impl AcpThreadView {
.map(|worktree| worktree.read(cx).abs_path())
.unwrap_or_else(|| paths::home_dir().as_path().into());
- let task = agent.new_thread(&root_dir, &project, cx);
+ let connect_task = agent.connect(&root_dir, &project, cx);
let load_task = cx.spawn_in(window, async move |this, cx| {
- let thread = match task.await {
+ let connection = match connect_task.await {
Ok(thread) => thread,
Err(err) => {
this.update(cx, |this, cx| {
@@ -222,48 +216,30 @@ impl AcpThreadView {
}
};
- let init_response = async {
- let resp = thread
- .read_with(cx, |thread, _cx| thread.initialize())?
- .await?;
- anyhow::Ok(resp)
- };
-
- let result = match init_response.await {
+ let result = match connection
+ .clone()
+ .new_thread(project.clone(), &root_dir, cx)
+ .await
+ {
Err(e) => {
let mut cx = cx.clone();
- if e.downcast_ref::<oneshot::Canceled>().is_some() {
- let child_status = thread
- .update(&mut cx, |thread, _| thread.child_status())
- .ok()
- .flatten();
- if let Some(child_status) = child_status {
- match child_status.await {
- Ok(_) => Err(e),
- Err(e) => Err(e),
- }
- } else {
- Err(e)
- }
- } else {
- Err(e)
- }
- }
- Ok(response) => {
- if !response.is_authenticated {
- this.update(cx, |this, _| {
- this.thread_state = ThreadState::Unauthenticated { thread };
+ if e.downcast_ref::<acp_thread::Unauthenticated>().is_some() {
+ this.update(&mut cx, |this, cx| {
+ this.thread_state = ThreadState::Unauthenticated { connection };
+ cx.notify();
})
.ok();
return;
- };
- Ok(())
+ } else {
+ Err(e)
+ }
}
+ Ok(session_id) => Ok(session_id),
};
this.update_in(cx, |this, window, cx| {
match result {
- Ok(()) => {
+ Ok(thread) => {
let thread_subscription =
cx.subscribe_in(&thread, window, Self::handle_thread_event);
@@ -305,10 +281,10 @@ impl AcpThreadView {
pub fn thread(&self) -> Option<&Entity<AcpThread>> {
match &self.thread_state {
- ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
- Some(thread)
- }
- ThreadState::Loading { .. } | ThreadState::LoadError(..) => None,
+ ThreadState::Ready { thread, .. } => Some(thread),
+ ThreadState::Unauthenticated { .. }
+ | ThreadState::Loading { .. }
+ | ThreadState::LoadError(..) => None,
}
}
@@ -325,7 +301,7 @@ impl AcpThreadView {
self.last_error.take();
if let Some(thread) = self.thread() {
- thread.update(cx, |thread, cx| thread.cancel(cx)).detach();
+ self._cancel_task = Some(thread.update(cx, |thread, cx| thread.cancel(cx)));
}
}
@@ -362,7 +338,7 @@ impl AcpThreadView {
self.last_error.take();
let mut ix = 0;
- let mut chunks: Vec<acp::UserMessageChunk> = Vec::new();
+ let mut chunks: Vec<acp::ContentBlock> = Vec::new();
let project = self.project.clone();
self.message_editor.update(cx, |editor, cx| {
let text = editor.text(cx);
@@ -374,12 +350,19 @@ impl AcpThreadView {
{
let crease_range = crease.range().to_offset(&snapshot.buffer_snapshot);
if crease_range.start > ix {
- chunks.push(acp::UserMessageChunk::Text {
- text: text[ix..crease_range.start].to_string(),
- });
+ chunks.push(text[ix..crease_range.start].into());
}
if let Some(abs_path) = project.read(cx).absolute_path(&project_path, cx) {
- chunks.push(acp::UserMessageChunk::Path { path: abs_path });
+ let path_str = abs_path.display().to_string();
+ chunks.push(acp::ContentBlock::ResourceLink(acp::ResourceLink {
+ uri: path_str.clone(),
+ name: path_str,
+ annotations: None,
+ description: None,
+ mime_type: None,
+ size: None,
+ title: None,
+ }));
}
ix = crease_range.end;
}
@@ -388,9 +371,7 @@ impl AcpThreadView {
if ix < text.len() {
let last_chunk = text[ix..].trim();
if !last_chunk.is_empty() {
- chunks.push(acp::UserMessageChunk::Text {
- text: last_chunk.into(),
- });
+ chunks.push(last_chunk.into());
}
}
})
@@ -401,8 +382,7 @@ impl AcpThreadView {
}
let Some(thread) = self.thread() else { return };
- let message = acp::SendUserMessageParams { chunks };
- let task = thread.update(cx, |thread, cx| thread.send(message.clone(), cx));
+ let task = thread.update(cx, |thread, cx| thread.send(chunks.clone(), cx));
cx.spawn(async move |this, cx| {
let result = task.await;
@@ -424,7 +404,7 @@ impl AcpThreadView {
editor.remove_creases(mention_set.lock().drain(), cx)
});
- self.message_history.borrow_mut().push(message);
+ self.message_history.borrow_mut().push(chunks);
}
fn previous_history_message(
@@ -490,7 +470,7 @@ impl AcpThreadView {
message_editor: Entity<Editor>,
mention_set: Arc<Mutex<MentionSet>>,
project: Entity<Project>,
- message: Option<&acp::SendUserMessageParams>,
+ message: Option<&Vec<acp::ContentBlock>>,
window: &mut Window,
cx: &mut Context<Self>,
) -> bool {
@@ -503,18 +483,19 @@ impl AcpThreadView {
let mut text = String::new();
let mut mentions = Vec::new();
- for chunk in &message.chunks {
+ for chunk in message {
match chunk {
- acp::UserMessageChunk::Text { text: chunk } => {
- text.push_str(&chunk);
+ acp::ContentBlock::Text(text_content) => {
+ text.push_str(&text_content.text);
}
- acp::UserMessageChunk::Path { path } => {
+ acp::ContentBlock::ResourceLink(resource_link) => {
+ let path = Path::new(&resource_link.uri);
let start = text.len();
- let content = MentionPath::new(path).to_string();
+ let content = MentionPath::new(&path).to_string();
text.push_str(&content);
let end = text.len();
if let Some(project_path) =
- project.read(cx).project_path_for_absolute_path(path, cx)
+ project.read(cx).project_path_for_absolute_path(&path, cx)
{
let filename: SharedString = path
.file_name()
@@ -525,6 +506,9 @@ impl AcpThreadView {
mentions.push((start..end, project_path, filename));
}
}
+ acp::ContentBlock::Image(_)
+ | acp::ContentBlock::Audio(_)
+ | acp::ContentBlock::Resource(_) => {}
}
}
@@ -590,71 +574,79 @@ impl AcpThreadView {
window: &mut Window,
cx: &mut Context<Self>,
) {
- let Some(multibuffer) = self.entry_diff_multibuffer(entry_ix, cx) else {
+ let Some(multibuffers) = self.entry_diff_multibuffers(entry_ix, cx) else {
return;
};
- if self.diff_editors.contains_key(&multibuffer.entity_id()) {
- return;
- }
+ let multibuffers = multibuffers.collect::<Vec<_>>();
- let editor = cx.new(|cx| {
- let mut editor = Editor::new(
- EditorMode::Full {
- scale_ui_elements_with_buffer_font_size: false,
- show_active_line_background: false,
- sized_by_content: true,
- },
- multibuffer.clone(),
- None,
- window,
- cx,
- );
- editor.set_show_gutter(false, cx);
- editor.disable_inline_diagnostics();
- editor.disable_expand_excerpt_buttons(cx);
- editor.set_show_vertical_scrollbar(false, cx);
- editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
- editor.set_soft_wrap_mode(SoftWrap::None, cx);
- editor.scroll_manager.set_forbid_vertical_scroll(true);
- editor.set_show_indent_guides(false, cx);
- editor.set_read_only(true);
- editor.set_show_breakpoints(false, cx);
- editor.set_show_code_actions(false, cx);
- editor.set_show_git_diff_gutter(false, cx);
- editor.set_expand_all_diff_hunks(cx);
- editor.set_text_style_refinement(TextStyleRefinement {
- font_size: Some(
- TextSize::Small
- .rems(cx)
- .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
- .into(),
- ),
- ..Default::default()
+ for multibuffer in multibuffers {
+ if self.diff_editors.contains_key(&multibuffer.entity_id()) {
+ return;
+ }
+
+ let editor = cx.new(|cx| {
+ let mut editor = Editor::new(
+ EditorMode::Full {
+ scale_ui_elements_with_buffer_font_size: false,
+ show_active_line_background: false,
+ sized_by_content: true,
+ },
+ multibuffer.clone(),
+ None,
+ window,
+ cx,
+ );
+ editor.set_show_gutter(false, cx);
+ editor.disable_inline_diagnostics();
+ editor.disable_expand_excerpt_buttons(cx);
+ editor.set_show_vertical_scrollbar(false, cx);
+ editor.set_minimap_visibility(MinimapVisibility::Disabled, window, cx);
+ editor.set_soft_wrap_mode(SoftWrap::None, cx);
+ editor.scroll_manager.set_forbid_vertical_scroll(true);
+ editor.set_show_indent_guides(false, cx);
+ editor.set_read_only(true);
+ editor.set_show_breakpoints(false, cx);
+ editor.set_show_code_actions(false, cx);
+ editor.set_show_git_diff_gutter(false, cx);
+ editor.set_expand_all_diff_hunks(cx);
+ editor.set_text_style_refinement(TextStyleRefinement {
+ font_size: Some(
+ TextSize::Small
+ .rems(cx)
+ .to_pixels(ThemeSettings::get_global(cx).agent_font_size(cx))
+ .into(),
+ ),
+ ..Default::default()
+ });
+ editor
});
- editor
- });
- let entity_id = multibuffer.entity_id();
- cx.observe_release(&multibuffer, move |this, _, _| {
- this.diff_editors.remove(&entity_id);
- })
- .detach();
+ let entity_id = multibuffer.entity_id();
+ cx.observe_release(&multibuffer, move |this, _, _| {
+ this.diff_editors.remove(&entity_id);
+ })
+ .detach();
- self.diff_editors.insert(entity_id, editor);
+ self.diff_editors.insert(entity_id, editor);
+ }
}
- fn entry_diff_multibuffer(&self, entry_ix: usize, cx: &App) -> Option<Entity<MultiBuffer>> {
+ fn entry_diff_multibuffers(
+ &self,
+ entry_ix: usize,
+ cx: &App,
+ ) -> Option<impl Iterator<Item = Entity<MultiBuffer>>> {
let entry = self.thread()?.read(cx).entries().get(entry_ix)?;
- entry.diff().map(|diff| diff.multibuffer.clone())
+ Some(entry.diffs().map(|diff| diff.multibuffer.clone()))
}
fn authenticate(&mut self, window: &mut Window, cx: &mut Context<Self>) {
- let Some(thread) = self.thread().cloned() else {
+ let ThreadState::Unauthenticated { ref connection } = self.thread_state else {
return;
};
self.last_error.take();
- let authenticate = thread.read(cx).authenticate();
+ let authenticate = connection.authenticate(cx);
self.auth_task = Some(cx.spawn_in(window, {
let project = self.project.clone();
let agent = self.agent.clone();
@@ -684,15 +676,16 @@ impl AcpThreadView {
fn authorize_tool_call(
&mut self,
- id: ToolCallId,
- outcome: acp::ToolCallConfirmationOutcome,
+ tool_call_id: acp::ToolCallId,
+ option_id: acp::PermissionOptionId,
+ option_kind: acp::PermissionOptionKind,
cx: &mut Context<Self>,
) {
let Some(thread) = self.thread() else {
return;
};
thread.update(cx, |thread, cx| {
- thread.authorize_tool_call(id, outcome, cx);
+ thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
});
cx.notify();
}
@@ -719,10 +712,12 @@ impl AcpThreadView {
.border_1()
.border_color(cx.theme().colors().border)
.text_xs()
- .child(self.render_markdown(
- message.content.clone(),
- user_message_markdown_style(window, cx),
- )),
+ .children(message.content.markdown().map(|md| {
+ self.render_markdown(
+ md.clone(),
+ user_message_markdown_style(window, cx),
+ )
+ })),
)
.into_any(),
AgentThreadEntry::AssistantMessage(AssistantMessage { chunks }) => {
@@ -730,20 +725,28 @@ impl AcpThreadView {
let message_body = v_flex()
.w_full()
.gap_2p5()
- .children(chunks.iter().enumerate().map(|(chunk_ix, chunk)| {
- match chunk {
- AssistantMessageChunk::Text { chunk } => self
- .render_markdown(chunk.clone(), style.clone())
- .into_any_element(),
- AssistantMessageChunk::Thought { chunk } => self.render_thinking_block(
- index,
- chunk_ix,
- chunk.clone(),
- window,
- cx,
- ),
- }
- }))
+ .children(chunks.iter().enumerate().filter_map(
+ |(chunk_ix, chunk)| match chunk {
+ AssistantMessageChunk::Message { block } => {
+ block.markdown().map(|md| {
+ self.render_markdown(md.clone(), style.clone())
+ .into_any_element()
+ })
+ }
+ AssistantMessageChunk::Thought { block } => {
+ block.markdown().map(|md| {
+ self.render_thinking_block(
+ index,
+ chunk_ix,
+ md.clone(),
+ window,
+ cx,
+ )
+ .into_any_element()
+ })
+ }
+ },
+ ))
.into_any();
v_flex()
@@ -871,7 +874,7 @@ impl AcpThreadView {
let status_icon = match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { .. } => None,
ToolCallStatus::Allowed {
- status: acp::ToolCallStatus::Running,
+ status: acp::ToolCallStatus::InProgress,
..
} => Some(
Icon::new(IconName::ArrowCircle)
@@ -885,13 +888,13 @@ impl AcpThreadView {
.into_any(),
),
ToolCallStatus::Allowed {
- status: acp::ToolCallStatus::Finished,
+ status: acp::ToolCallStatus::Completed,
..
} => None,
ToolCallStatus::Rejected
| ToolCallStatus::Canceled
| ToolCallStatus::Allowed {
- status: acp::ToolCallStatus::Error,
+ status: acp::ToolCallStatus::Failed,
..
} => Some(
Icon::new(IconName::X)
@@ -909,34 +912,9 @@ impl AcpThreadView {
.any(|content| matches!(content, ToolCallContent::Diff { .. })),
};
- let is_collapsible = tool_call.content.is_some() && !needs_confirmation;
+ let is_collapsible = !tool_call.content.is_empty() && !needs_confirmation;
let is_open = !is_collapsible || self.expanded_tool_calls.contains(&tool_call.id);
- let content = if is_open {
- match &tool_call.status {
- ToolCallStatus::WaitingForConfirmation { confirmation, .. } => {
- Some(self.render_tool_call_confirmation(
- tool_call.id,
- confirmation,
- tool_call.content.as_ref(),
- window,
- cx,
- ))
- }
- ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
- tool_call.content.as_ref().map(|content| {
- div()
- .py_1p5()
- .child(self.render_tool_call_content(content, window, cx))
- .into_any_element()
- })
- }
- ToolCallStatus::Rejected => None,
- }
- } else {
- None
- };
-
v_flex()
.when(needs_confirmation, |this| {
this.rounded_lg()
@@ -976,9 +954,17 @@ impl AcpThreadView {
})
.gap_1p5()
.child(
- Icon::new(tool_call.icon)
- .size(IconSize::Small)
- .color(Color::Muted),
+ Icon::new(match tool_call.kind {
+ acp::ToolKind::Read => IconName::ToolRead,
+ acp::ToolKind::Edit => IconName::ToolPencil,
+ acp::ToolKind::Search => IconName::ToolSearch,
+ acp::ToolKind::Execute => IconName::ToolTerminal,
+ acp::ToolKind::Think => IconName::ToolBulb,
+ acp::ToolKind::Fetch => IconName::ToolWeb,
+ acp::ToolKind::Other => IconName::ToolHammer,
+ })
+ .size(IconSize::Small)
+ .color(Color::Muted),
)
.child(if tool_call.locations.len() == 1 {
let name = tool_call.locations[0]
@@ -1023,16 +1009,16 @@ impl AcpThreadView {
.gap_0p5()
.when(is_collapsible, |this| {
this.child(
- Disclosure::new(("expand", tool_call.id.0), is_open)
+ Disclosure::new(("expand", entry_ix), is_open)
.opened_icon(IconName::ChevronUp)
.closed_icon(IconName::ChevronDown)
.on_click(cx.listener({
- let id = tool_call.id;
+ let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
} else {
- this.expanded_tool_calls.insert(id);
+ this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
@@ -1042,12 +1028,12 @@ impl AcpThreadView {
.children(status_icon),
)
.on_click(cx.listener({
- let id = tool_call.id;
+ let id = tool_call.id.clone();
move |this: &mut Self, _, _, cx: &mut Context<Self>| {
if is_open {
this.expanded_tool_calls.remove(&id);
} else {
- this.expanded_tool_calls.insert(id);
+ this.expanded_tool_calls.insert(id.clone());
}
cx.notify();
}
@@ -1055,7 +1041,7 @@ impl AcpThreadView {
)
.when(is_open, |this| {
this.child(
- div()
+ v_flex()
.text_xs()
.when(is_collapsible, |this| {
this.mt_1()
@@ -1064,7 +1050,44 @@ impl AcpThreadView {
.bg(cx.theme().colors().editor_background)
.rounded_lg()
})
- .children(content),
+ .map(|this| {
+ if is_open {
+ match &tool_call.status {
+ ToolCallStatus::WaitingForConfirmation { options, .. } => this
+ .children(tool_call.content.iter().map(|content| {
+ div()
+ .py_1p5()
+ .child(
+ self.render_tool_call_content(
+ content, window, cx,
+ ),
+ )
+ .into_any_element()
+ }))
+ .child(self.render_permission_buttons(
+ options,
+ entry_ix,
+ tool_call.id.clone(),
+ cx,
+ )),
+ ToolCallStatus::Allowed { .. } | ToolCallStatus::Canceled => {
+ this.children(tool_call.content.iter().map(|content| {
+ div()
+ .py_1p5()
+ .child(
+ self.render_tool_call_content(
+ content, window, cx,
+ ),
+ )
+ .into_any_element()
+ }))
+ }
+ ToolCallStatus::Rejected => this,
+ }
+ } else {
+ this
+ }
+ }),
)
})
}
@@ -1076,14 +1099,20 @@ impl AcpThreadView {
cx: &Context<Self>,
) -> AnyElement {
match content {
- ToolCallContent::Markdown { markdown } => {
- div()
- .p_2()
- .child(self.render_markdown(
- markdown.clone(),
- default_markdown_style(false, window, cx),
- ))
- .into_any_element()
+ ToolCallContent::ContentBlock { content } => {
+ if let Some(md) = content.markdown() {
+ div()
+ .p_2()
+ .child(
+ self.render_markdown(
+ md.clone(),
+ default_markdown_style(false, window, cx),
+ ),
+ )
+ .into_any_element()
+ } else {
+ Empty.into_any_element()
+ }
}
ToolCallContent::Diff {
diff: Diff { multibuffer, .. },
@@ -1092,223 +1121,53 @@ impl AcpThreadView {
}
}
- fn render_tool_call_confirmation(
+ fn render_permission_buttons(
&self,
- tool_call_id: ToolCallId,
- confirmation: &ToolCallConfirmation,
- content: Option<&ToolCallContent>,
- window: &Window,
- cx: &Context<Self>,
- ) -> AnyElement {
- let confirmation_container = v_flex().mt_1().py_1p5();
-
- match confirmation {
- ToolCallConfirmation::Edit { description } => confirmation_container
- .child(
- div()
- .px_2()
- .children(description.clone().map(|description| {
- self.render_markdown(
- description,
- default_markdown_style(false, window, cx),
- )
- })),
- )
- .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
- .child(self.render_confirmation_buttons(
- &[AlwaysAllowOption {
- id: "always_allow",
- label: "Always Allow Edits".into(),
- outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
- }],
- tool_call_id,
- cx,
- ))
- .into_any(),
- ToolCallConfirmation::Execute {
- command,
- root_command,
- description,
- } => confirmation_container
- .child(v_flex().px_2().pb_1p5().child(command.clone()).children(
- description.clone().map(|description| {
- self.render_markdown(description, default_markdown_style(false, window, cx))
- .on_url_click({
- let workspace = self.workspace.clone();
- move |text, window, cx| {
- Self::open_link(text, &workspace, window, cx);
- }
- })
- }),
- ))
- .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
- .child(self.render_confirmation_buttons(
- &[AlwaysAllowOption {
- id: "always_allow",
- label: format!("Always Allow {root_command}").into(),
- outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
- }],
- tool_call_id,
- cx,
- ))
- .into_any(),
- ToolCallConfirmation::Mcp {
- server_name,
- tool_name: _,
- tool_display_name,
- description,
- } => confirmation_container
- .child(
- v_flex()
- .px_2()
- .pb_1p5()
- .child(format!("{server_name} - {tool_display_name}"))
- .children(description.clone().map(|description| {
- self.render_markdown(
- description,
- default_markdown_style(false, window, cx),
- )
- })),
- )
- .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
- .child(self.render_confirmation_buttons(
- &[
- AlwaysAllowOption {
- id: "always_allow_server",
- label: format!("Always Allow {server_name}").into(),
- outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowMcpServer,
- },
- AlwaysAllowOption {
- id: "always_allow_tool",
- label: format!("Always Allow {tool_display_name}").into(),
- outcome: acp::ToolCallConfirmationOutcome::AlwaysAllowTool,
- },
- ],
- tool_call_id,
- cx,
- ))
- .into_any(),
- ToolCallConfirmation::Fetch { description, urls } => confirmation_container
- .child(
- v_flex()
- .px_2()
- .pb_1p5()
- .gap_1()
- .children(urls.iter().map(|url| {
- h_flex().child(
- Button::new(url.clone(), url)
- .icon(IconName::ArrowUpRight)
- .icon_color(Color::Muted)
- .icon_size(IconSize::XSmall)
- .on_click({
- let url = url.clone();
- move |_, _, cx| cx.open_url(&url)
- }),
- )
- }))
- .children(description.clone().map(|description| {
- self.render_markdown(
- description,
- default_markdown_style(false, window, cx),
- )
- })),
- )
- .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
- .child(self.render_confirmation_buttons(
- &[AlwaysAllowOption {
- id: "always_allow",
- label: "Always Allow".into(),
- outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
- }],
- tool_call_id,
- cx,
- ))
- .into_any(),
- ToolCallConfirmation::Other { description } => confirmation_container
- .child(v_flex().px_2().pb_1p5().child(self.render_markdown(
- description.clone(),
- default_markdown_style(false, window, cx),
- )))
- .children(content.map(|content| self.render_tool_call_content(content, window, cx)))
- .child(self.render_confirmation_buttons(
- &[AlwaysAllowOption {
- id: "always_allow",
- label: "Always Allow".into(),
- outcome: acp::ToolCallConfirmationOutcome::AlwaysAllow,
- }],
- tool_call_id,
- cx,
- ))
- .into_any(),
- }
- }
-
- fn render_confirmation_buttons(
- &self,
- always_allow_options: &[AlwaysAllowOption],
- tool_call_id: ToolCallId,
+ options: &[acp::PermissionOption],
+ entry_ix: usize,
+ tool_call_id: acp::ToolCallId,
cx: &Context<Self>,
) -> Div {
h_flex()
- .pt_1p5()
+ .py_1p5()
.px_1p5()
.gap_1()
.justify_end()
.border_t_1()
.border_color(self.tool_card_border_color(cx))
- .when(self.agent.supports_always_allow(), |this| {
- this.children(always_allow_options.into_iter().map(|always_allow_option| {
- let outcome = always_allow_option.outcome;
- Button::new(
- (always_allow_option.id, tool_call_id.0),
- always_allow_option.label.clone(),
- )
- .icon(IconName::CheckDouble)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Success)
- .on_click(cx.listener({
- let id = tool_call_id;
- move |this, _, _, cx| {
- this.authorize_tool_call(id, outcome, cx);
+ .children(options.iter().map(|option| {
+ let option_id = SharedString::from(option.id.0.clone());
+ Button::new((option_id, entry_ix), option.label.clone())
+ .map(|this| match option.kind {
+ acp::PermissionOptionKind::AllowOnce => {
+ this.icon(IconName::Check).icon_color(Color::Success)
}
- }))
- }))
- })
- .child(
- Button::new(("allow", tool_call_id.0), "Allow")
- .icon(IconName::Check)
- .icon_position(IconPosition::Start)
- .icon_size(IconSize::XSmall)
- .icon_color(Color::Success)
- .on_click(cx.listener({
- let id = tool_call_id;
- move |this, _, _, cx| {
- this.authorize_tool_call(
- id,
- acp::ToolCallConfirmationOutcome::Allow,
- cx,
- );
+ acp::PermissionOptionKind::AllowAlways => {
+ this.icon(IconName::CheckDouble).icon_color(Color::Success)
}
- })),
- )
- .child(
- Button::new(("reject", tool_call_id.0), "Reject")
- .icon(IconName::X)
+ acp::PermissionOptionKind::RejectOnce => {
+ this.icon(IconName::X).icon_color(Color::Error)
+ }
+ acp::PermissionOptionKind::RejectAlways => {
+ this.icon(IconName::X).icon_color(Color::Error)
+ }
+ })
.icon_position(IconPosition::Start)
.icon_size(IconSize::XSmall)
- .icon_color(Color::Error)
.on_click(cx.listener({
- let id = tool_call_id;
+ let tool_call_id = tool_call_id.clone();
+ let option_id = option.id.clone();
+ let option_kind = option.kind;
move |this, _, _, cx| {
this.authorize_tool_call(
- id,
- acp::ToolCallConfirmationOutcome::Reject,
+ tool_call_id.clone(),
+ option_id.clone(),
+ option_kind,
cx,
);
}
- })),
- )
+ }))
+ }))
}
fn render_diff_editor(&self, multibuffer: &Entity<MultiBuffer>) -> AnyElement {
@@ -2245,12 +2104,11 @@ impl AcpThreadView {
.languages
.language_for_name("Markdown");
- let (thread_summary, markdown) = match &self.thread_state {
- ThreadState::Ready { thread, .. } | ThreadState::Unauthenticated { thread } => {
- let thread = thread.read(cx);
- (thread.title().to_string(), thread.to_markdown(cx))
- }
- ThreadState::Loading { .. } | ThreadState::LoadError(..) => return Task::ready(Ok(())),
+ let (thread_summary, markdown) = if let Some(thread) = self.thread() {
+ let thread = thread.read(cx);
+ (thread.title().to_string(), thread.to_markdown(cx))
+ } else {
+ return Task::ready(Ok(()));
};
window.spawn(cx, async move |cx| {
@@ -1506,8 +1506,7 @@ impl AgentDiff {
.read(cx)
.entries()
.last()
- .and_then(|entry| entry.diff())
- .is_some()
+ .map_or(false, |entry| entry.diffs().next().is_some())
{
self.update_reviewing_editors(workspace, window, cx);
}
@@ -1517,8 +1516,7 @@ impl AgentDiff {
.read(cx)
.entries()
.get(*ix)
- .and_then(|entry| entry.diff())
- .is_some()
+ .map_or(false, |entry| entry.diffs().next().is_some())
{
self.update_reviewing_editors(workspace, window, cx);
}
@@ -440,7 +440,7 @@ pub struct AgentPanel {
local_timezone: UtcOffset,
active_view: ActiveView,
acp_message_history:
- Rc<RefCell<crate::acp::MessageHistory<agentic_coding_protocol::SendUserMessageParams>>>,
+ Rc<RefCell<crate::acp::MessageHistory<Vec<agent_client_protocol::ContentBlock>>>>,
previous_view: Option<ActiveView>,
history_store: Entity<HistoryStore>,
history: Entity<ThreadHistory>,
@@ -1,6 +1,6 @@
use anyhow::{Context as _, Result, anyhow};
use collections::HashMap;
-use futures::{FutureExt, StreamExt, channel::oneshot, select};
+use futures::{FutureExt, StreamExt, channel::oneshot, future, select};
use gpui::{AppContext as _, AsyncApp, BackgroundExecutor, Task};
use parking_lot::Mutex;
use postage::barrier;
@@ -10,15 +10,19 @@ use smol::channel;
use std::{
fmt,
path::PathBuf,
+ pin::pin,
sync::{
Arc,
atomic::{AtomicI32, Ordering::SeqCst},
},
time::{Duration, Instant},
};
-use util::TryFutureExt;
+use util::{ResultExt, TryFutureExt};
-use crate::transport::{StdioTransport, Transport};
+use crate::{
+ transport::{StdioTransport, Transport},
+ types::{CancelledParams, ClientNotification, Notification as _, notifications::Cancelled},
+};
const JSON_RPC_VERSION: &str = "2.0";
const REQUEST_TIMEOUT: Duration = Duration::from_secs(60);
@@ -32,6 +36,7 @@ pub const INTERNAL_ERROR: i32 = -32603;
type ResponseHandler = Box<dyn Send + FnOnce(Result<String, Error>)>;
type NotificationHandler = Box<dyn Send + FnMut(Value, AsyncApp)>;
+type RequestHandler = Box<dyn Send + FnMut(RequestId, &RawValue, AsyncApp)>;
#[derive(Debug, Clone, Eq, PartialEq, Hash, Serialize, Deserialize)]
#[serde(untagged)]
@@ -78,6 +83,15 @@ pub struct Request<'a, T> {
pub params: T,
}
+#[derive(Serialize, Deserialize)]
+pub struct AnyRequest<'a> {
+ pub jsonrpc: &'a str,
+ pub id: RequestId,
+ pub method: &'a str,
+ #[serde(skip_serializing_if = "is_null_value")]
+ pub params: Option<&'a RawValue>,
+}
+
#[derive(Serialize, Deserialize)]
struct AnyResponse<'a> {
jsonrpc: &'a str,
@@ -176,15 +190,23 @@ impl Client {
Arc::new(Mutex::new(HashMap::<_, NotificationHandler>::default()));
let response_handlers =
Arc::new(Mutex::new(Some(HashMap::<_, ResponseHandler>::default())));
+ let request_handlers = Arc::new(Mutex::new(HashMap::<_, RequestHandler>::default()));
let receive_input_task = cx.spawn({
let notification_handlers = notification_handlers.clone();
let response_handlers = response_handlers.clone();
+ let request_handlers = request_handlers.clone();
let transport = transport.clone();
async move |cx| {
- Self::handle_input(transport, notification_handlers, response_handlers, cx)
- .log_err()
- .await
+ Self::handle_input(
+ transport,
+ notification_handlers,
+ request_handlers,
+ response_handlers,
+ cx,
+ )
+ .log_err()
+ .await
}
});
let receive_err_task = cx.spawn({
@@ -230,13 +252,24 @@ impl Client {
async fn handle_input(
transport: Arc<dyn Transport>,
notification_handlers: Arc<Mutex<HashMap<&'static str, NotificationHandler>>>,
+ request_handlers: Arc<Mutex<HashMap<&'static str, RequestHandler>>>,
response_handlers: Arc<Mutex<Option<HashMap<RequestId, ResponseHandler>>>>,
cx: &mut AsyncApp,
) -> anyhow::Result<()> {
let mut receiver = transport.receive();
while let Some(message) = receiver.next().await {
- if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
+ log::trace!("recv: {}", &message);
+ if let Ok(request) = serde_json::from_str::<AnyRequest>(&message) {
+ let mut request_handlers = request_handlers.lock();
+ if let Some(handler) = request_handlers.get_mut(request.method) {
+ handler(
+ request.id,
+ request.params.unwrap_or(RawValue::NULL),
+ cx.clone(),
+ );
+ }
+ } else if let Ok(response) = serde_json::from_str::<AnyResponse>(&message) {
if let Some(handlers) = response_handlers.lock().as_mut() {
if let Some(handler) = handlers.remove(&response.id) {
handler(Ok(message.to_string()));
@@ -247,6 +280,8 @@ impl Client {
if let Some(handler) = notification_handlers.get_mut(notification.method.as_str()) {
handler(notification.params.unwrap_or(Value::Null), cx.clone());
}
+ } else {
+ log::error!("Unhandled JSON from context_server: {}", message);
}
}
@@ -294,6 +329,24 @@ impl Client {
&self,
method: &str,
params: impl Serialize,
+ ) -> Result<T> {
+ self.request_impl(method, params, None).await
+ }
+
+ pub async fn cancellable_request<T: DeserializeOwned>(
+ &self,
+ method: &str,
+ params: impl Serialize,
+ cancel_rx: oneshot::Receiver<()>,
+ ) -> Result<T> {
+ self.request_impl(method, params, Some(cancel_rx)).await
+ }
+
+ pub async fn request_impl<T: DeserializeOwned>(
+ &self,
+ method: &str,
+ params: impl Serialize,
+ cancel_rx: Option<oneshot::Receiver<()>>,
) -> Result<T> {
let id = self.next_id.fetch_add(1, SeqCst);
let request = serde_json::to_string(&Request {
@@ -330,6 +383,16 @@ impl Client {
send?;
let mut timeout = executor.timer(REQUEST_TIMEOUT).fuse();
+ let mut cancel_fut = pin!(
+ match cancel_rx {
+ Some(rx) => future::Either::Left(async {
+ rx.await.log_err();
+ }),
+ None => future::Either::Right(future::pending()),
+ }
+ .fuse()
+ );
+
select! {
response = rx.fuse() => {
let elapsed = started.elapsed();
@@ -348,6 +411,16 @@ impl Client {
Err(_) => anyhow::bail!("cancelled")
}
}
+ _ = cancel_fut => {
+ self.notify(
+ Cancelled::METHOD,
+ ClientNotification::Cancelled(CancelledParams {
+ request_id: RequestId::Int(id),
+ reason: None
+ })
+ ).log_err();
+ anyhow::bail!("Request cancelled")
+ }
_ = timeout => {
log::error!("cancelled csp request task for {method:?} id {id} which took over {:?}", REQUEST_TIMEOUT);
anyhow::bail!("Context server request timeout");
@@ -6,6 +6,9 @@
//! of messages.
use anyhow::Result;
+use futures::channel::oneshot;
+use gpui::AsyncApp;
+use serde_json::Value;
use crate::client::Client;
use crate::types::{self, Notification, Request};
@@ -95,7 +98,24 @@ impl InitializedContextServerProtocol {
self.inner.request(T::METHOD, params).await
}
+ pub async fn cancellable_request<T: Request>(
+ &self,
+ params: T::Params,
+ cancel_rx: oneshot::Receiver<()>,
+ ) -> Result<T::Response> {
+ self.inner
+ .cancellable_request(T::METHOD, params, cancel_rx)
+ .await
+ }
+
pub fn notify<T: Notification>(&self, params: T::Params) -> Result<()> {
self.inner.notify(T::METHOD, params)
}
+
+ pub fn on_notification<F>(&self, method: &'static str, f: F)
+ where
+ F: 'static + Send + FnMut(Value, AsyncApp),
+ {
+ self.inner.on_notification(method, f);
+ }
}
@@ -3,6 +3,8 @@ use serde::de::DeserializeOwned;
use serde::{Deserialize, Serialize};
use url::Url;
+use crate::client::RequestId;
+
pub const LATEST_PROTOCOL_VERSION: &str = "2025-03-26";
pub const VERSION_2024_11_05: &str = "2024-11-05";
@@ -100,6 +102,7 @@ pub mod notifications {
notification!("notifications/initialized", Initialized, ());
notification!("notifications/progress", Progress, ProgressParams);
notification!("notifications/message", Message, MessageParams);
+ notification!("notifications/cancelled", Cancelled, CancelledParams);
notification!(
"notifications/resources/updated",
ResourcesUpdated,
@@ -617,11 +620,14 @@ pub enum ClientNotification {
Initialized,
Progress(ProgressParams),
RootsListChanged,
- Cancelled {
- request_id: String,
- #[serde(skip_serializing_if = "Option::is_none")]
- reason: Option<String>,
- },
+ Cancelled(CancelledParams),
+}
+
+#[derive(Debug, Serialize, Deserialize)]
+pub struct CancelledParams {
+ pub request_id: RequestId,
+ #[serde(skip_serializing_if = "Option::is_none")]
+ pub reason: Option<String>,
}
#[derive(Debug, Serialize, Deserialize)]