Co-authored-by: Agus Zubiaga <agus@zed.dev>
This commit is contained in:
Ben Brandt 2025-07-23 16:21:20 +02:00
parent edbcc71b87
commit ebf94e20c9
No known key found for this signature in database
GPG key ID: D4618C5D3B500571
3 changed files with 136 additions and 51 deletions

View file

@ -221,8 +221,8 @@ impl ToolCall {
#[derive(Debug)] #[derive(Debug)]
pub enum ToolCallStatus { pub enum ToolCallStatus {
WaitingForConfirmation { WaitingForConfirmation {
possible_grants: Vec<acp::Grant>, options: Vec<acp::PermissionOption>,
respond_tx: oneshot::Sender<acp::GrantId>, respond_tx: oneshot::Sender<acp::PermissionOutcome>,
}, },
Allowed { Allowed {
status: acp::ToolCallStatus, status: acp::ToolCallStatus,
@ -550,6 +550,7 @@ pub struct AcpThread {
send_task: Option<Task<()>>, send_task: Option<Task<()>>,
connection: Arc<dyn AgentConnection>, connection: Arc<dyn AgentConnection>,
child_status: Option<Task<Result<()>>>, child_status: Option<Task<Result<()>>>,
session_id: acp::SessionId,
} }
pub enum AcpThreadEvent { pub enum AcpThreadEvent {
@ -595,6 +596,7 @@ impl AcpThread {
title: SharedString, title: SharedString,
child_status: Option<Task<Result<()>>>, child_status: Option<Task<Result<()>>>,
project: Entity<Project>, project: Entity<Project>,
session_id: acp::SessionId,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> Self { ) -> Self {
let action_log = cx.new(|_| ActionLog::new(project.clone())); let action_log = cx.new(|_| ActionLog::new(project.clone()));
@ -609,19 +611,7 @@ impl AcpThread {
send_task: None, send_task: None,
connection: Arc::new(connection), connection: Arc::new(connection),
child_status, child_status,
} session_id,
}
/// Send a request to the agent and wait for a response.
pub fn request<R: acp_old::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)?)
} }
} }
@ -778,13 +768,13 @@ impl AcpThread {
pub fn request_tool_call_permission( pub fn request_tool_call_permission(
&mut self, &mut self,
tool_call: acp::ToolCall, tool_call: acp::ToolCall,
possible_grants: Vec<acp::Grant>, options: Vec<acp::PermissionOption>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> oneshot::Receiver<acp::GrantId> { ) -> oneshot::Receiver<acp::PermissionOutcome> {
let (tx, rx) = oneshot::channel(); let (tx, rx) = oneshot::channel();
let status = ToolCallStatus::WaitingForConfirmation { let status = ToolCallStatus::WaitingForConfirmation {
possible_grants, options,
respond_tx: tx, respond_tx: tx,
}; };
@ -812,25 +802,32 @@ impl AcpThread {
pub fn authorize_tool_call( pub fn authorize_tool_call(
&mut self, &mut self,
id: acp::ToolCallId, id: acp::ToolCallId,
grant: acp::Grant, option: acp::PermissionOption,
cx: &mut Context<Self>, 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; return;
}; };
let new_status = if grant.is_allowed { let new_status = match option.kind {
ToolCallStatus::Allowed { acp::PermissionOptionKind::RejectOnce | acp::PermissionOptionKind::RejectAlways => {
status: acp::ToolCallStatus::InProgress, ToolCallStatus::Rejected
}
acp::PermissionOptionKind::AllowOnce | acp::PermissionOptionKind::AllowAlways => {
ToolCallStatus::Allowed {
status: acp::ToolCallStatus::InProgress,
}
} }
} else {
ToolCallStatus::Rejected
}; };
let curr_status = mem::replace(&mut call.status, new_status); let curr_status = mem::replace(&mut call.status, new_status);
if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status { if let ToolCallStatus::WaitingForConfirmation { respond_tx, .. } = curr_status {
respond_tx.send(grant.id).log_err(); respond_tx
.send(acp::PermissionOutcome::Selected {
option_id: option.id,
})
.log_err();
} else if cfg!(debug_assertions) { } else if cfg!(debug_assertions) {
panic!("tried to authorize an already authorized tool call"); panic!("tried to authorize an already authorized tool call");
} }
@ -941,8 +938,11 @@ impl AcpThread {
message: Vec<acp::ContentBlock>, message: Vec<acp::ContentBlock>,
cx: &mut Context<Self>, cx: &mut Context<Self>,
) -> BoxFuture<'static, Result<(), acp_old::Error>> { ) -> BoxFuture<'static, Result<(), acp_old::Error>> {
let block = let block = ContentBlock::new_combined(
ContentBlock::new_combined(message.clone(), self.project.read(cx).languages(), cx); message.clone(),
self.project.read(cx).languages().clone(),
cx,
);
self.push_entry( self.push_entry(
AgentThreadEntry::UserMessage(UserMessage { content: block }), AgentThreadEntry::UserMessage(UserMessage { content: block }),
cx, cx,
@ -955,7 +955,14 @@ impl AcpThread {
async { async {
cancel.await.log_err(); cancel.await.log_err();
let result = this.update(cx, |this, _| this.request(message))?.await; let result = this
.update(cx, |this, _| {
this.connection.prompt(acp::PromptToolArguments {
prompt: message,
session_id: this.session_id.clone(),
})
})?
.await;
tx.send(result).log_err(); tx.send(result).log_err();
this.update(cx, |this, _cx| this.send_task.take())?; this.update(cx, |this, _cx| this.send_task.take())?;
anyhow::Ok(()) anyhow::Ok(())
@ -975,7 +982,7 @@ impl AcpThread {
pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp_old::Error>> { pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp_old::Error>> {
if self.send_task.take().is_some() { if self.send_task.take().is_some() {
let request = self.request(acp_old::CancelSendMessageParams); let request = self.connection.cancel();
cx.spawn(async move |this, cx| { cx.spawn(async move |this, cx| {
request.await?; request.await?;
this.update(cx, |this, _cx| { this.update(cx, |this, _cx| {
@ -994,17 +1001,10 @@ impl AcpThread {
mem::replace(&mut call.status, ToolCallStatus::Canceled); mem::replace(&mut call.status, ToolCallStatus::Canceled);
if let ToolCallStatus::WaitingForConfirmation { if let ToolCallStatus::WaitingForConfirmation {
respond_tx, respond_tx, ..
possible_grants,
} = curr_status } = curr_status
{ {
if let Some(grant_id) = possible_grants respond_tx.send(acp::PermissionOutcome::Canceled).ok();
.iter()
.find_map(|g| (!g.is_allowed).then(|| g.id.clone()))
{
// todo! do we need a way to cancel rather than reject?
respond_tx.send(grant_id).ok();
}
} }
} }
} }
@ -1171,12 +1171,17 @@ impl AcpThread {
pub struct OldAcpClientDelegate { pub struct OldAcpClientDelegate {
thread: WeakEntity<AcpThread>, thread: WeakEntity<AcpThread>,
cx: AsyncApp, cx: AsyncApp,
next_tool_call_id: Rc<RefCell<u64>>,
// sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>, // sent_buffer_versions: HashMap<Entity<Buffer>, HashMap<u64, BufferSnapshot>>,
} }
impl OldAcpClientDelegate { impl OldAcpClientDelegate {
pub fn new(thread: WeakEntity<AcpThread>, cx: AsyncApp) -> Self { pub fn new(thread: WeakEntity<AcpThread>, cx: AsyncApp) -> Self {
Self { thread, cx } Self {
thread,
cx,
next_tool_call_id: Rc::new(RefCell::new(0)),
}
} }
pub async fn clear_completed_plan_entries(&self) -> Result<()> { pub async fn clear_completed_plan_entries(&self) -> Result<()> {
@ -1196,10 +1201,11 @@ impl OldAcpClientDelegate {
confirmation: acp_old::ToolCallConfirmation, confirmation: acp_old::ToolCallConfirmation,
) -> Result<acp_old::ToolCallConfirmationOutcome> { ) -> Result<acp_old::ToolCallConfirmationOutcome> {
let cx = &mut self.cx.clone(); let cx = &mut self.cx.clone();
let ToolCallRequest { outcome, .. } = cx let ToolCallRequest { outcome, .. } = cx
.update(|cx| { .update(|cx| {
self.thread.update(cx, |thread, cx| { self.thread.update(cx, |thread, cx| {
thread.request_tool_call_confirmation(tool_call_id, confirmation, cx) thread.request_tool_call_permission(acp_new_tool_call, confirmation, cx)
}) })
})? })?
.context("Failed to update thread")??; .context("Failed to update thread")??;
@ -1264,10 +1270,26 @@ impl acp_old::Client for OldAcpClientDelegate {
request: acp_old::PushToolCallParams, request: acp_old::PushToolCallParams,
) -> Result<acp_old::PushToolCallResponse, acp_old::Error> { ) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
let cx = &mut self.cx.clone(); let cx = &mut self.cx.clone();
let new_id = acp::ToolCallId(
util::post_inc(self.next_tool_call_id.borrow_mut())
.to_string()
.into(),
);
let new_tool_call = acp::ToolCall {
id: new_id,
label: request.label,
kind: acp_kind_from_icon(request.icon),
status: acp::ToolCallStatus::InProgress,
content: into_new_acp_content(request.content),
locations: request.locations.into_iter().map(into_new_acp_location).collect(),
};
let id = cx let id = cx
.update(|cx| { .update(|cx| {
self.thread self.thread
.update(cx, |thread, cx| thread.push_tool_call(request, cx)) .update(cx, |thread, cx| thread.update_tool_call(request, cx))
})? })?
.context("Failed to update thread")?; .context("Failed to update thread")?;

View file

@ -1,20 +1,83 @@
use agentic_coding_protocol as acp_old; use agent_client_protocol as acp;
use agentic_coding_protocol::{self as acp_old, AgentRequest};
use anyhow::Result; use anyhow::Result;
use futures::future::{FutureExt as _, LocalBoxFuture}; use futures::future::{FutureExt as _, LocalBoxFuture};
pub trait AgentConnection { pub trait AgentConnection {
fn request_any( fn new_session(
&self, &self,
params: acp_old::AnyAgentRequest, params: acp::NewSessionToolArguments,
) -> LocalBoxFuture<'static, Result<acp_old::AnyAgentResult>>; ) -> LocalBoxFuture<'static, Result<acp::SessionId>>;
fn authenticate(&self) -> LocalBoxFuture<'static, Result<()>>;
fn prompt(&self, params: acp::PromptToolArguments) -> LocalBoxFuture<'static, Result<()>>;
fn cancel(&self) -> LocalBoxFuture<'static, Result<()>>;
} }
impl AgentConnection for acp_old::AgentConnection { impl AgentConnection for acp_old::AgentConnection {
fn request_any( fn new_session(
&self, &self,
params: acp_old::AnyAgentRequest, _params: acp::NewSessionToolArguments,
) -> LocalBoxFuture<'static, Result<acp_old::AnyAgentResult>> { ) -> LocalBoxFuture<'static, Result<acp::SessionId>> {
let task = self.request_any(params); let task = self.request_any(
async move { Ok(task.await?) }.boxed_local() acp_old::InitializeParams {
protocol_version: acp_old::ProtocolVersion::latest(),
}
.into_any(),
);
async move {
let result = task.await?;
let result = acp_old::InitializeParams::response_from_any(result)?;
if !result.is_authenticated {
anyhow::bail!("Not authenticated");
}
Ok(acp::SessionId("acp-old-no-id".into()))
}
.boxed_local()
}
fn authenticate(&self) -> LocalBoxFuture<'static, Result<()>> {
let task = self.request_any(acp_old::AuthenticateParams.into_any());
async move {
task.await?;
anyhow::Ok(())
}
.boxed_local()
}
fn prompt(&self, params: acp::PromptToolArguments) -> LocalBoxFuture<'static, 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.request_any(acp_old::SendUserMessageParams { chunks }.into_any());
async move {
task.await?;
anyhow::Ok(())
}
.boxed_local()
}
fn cancel(&self) -> LocalBoxFuture<'static, Result<()>> {
let task = self.request_any(acp_old::CancelSendMessageParams.into_any());
async move {
task.await?;
anyhow::Ok(())
}
.boxed_local()
} }
} }

View file

@ -911,7 +911,7 @@ impl AcpThreadView {
let content = if is_open { let content = if is_open {
match &tool_call.status { match &tool_call.status {
ToolCallStatus::WaitingForConfirmation { ToolCallStatus::WaitingForConfirmation {
possible_grants, options,
respond_tx, respond_tx,
} => { } => {
// Some(self.render_tool_call_confirmation( // Some(self.render_tool_call_confirmation(