wip

Ben Brandt and Agus Zubiaga created

Co-authored-by: Agus Zubiaga <agus@zed.dev>

Change summary

crates/acp_thread/src/acp_thread.rs    | 104 ++++++++++++++++-----------
crates/acp_thread/src/connection.rs    |  81 +++++++++++++++++++--
crates/agent_ui/src/acp/thread_view.rs |   2 
3 files changed, 136 insertions(+), 51 deletions(-)

Detailed changes

crates/acp_thread/src/acp_thread.rs 🔗

@@ -221,8 +221,8 @@ impl ToolCall {
 #[derive(Debug)]
 pub enum ToolCallStatus {
     WaitingForConfirmation {
-        possible_grants: Vec<acp::Grant>,
-        respond_tx: oneshot::Sender<acp::GrantId>,
+        options: Vec<acp::PermissionOption>,
+        respond_tx: oneshot::Sender<acp::PermissionOutcome>,
     },
     Allowed {
         status: acp::ToolCallStatus,
@@ -550,6 +550,7 @@ pub struct AcpThread {
     send_task: Option<Task<()>>,
     connection: Arc<dyn AgentConnection>,
     child_status: Option<Task<Result<()>>>,
+    session_id: acp::SessionId,
 }
 
 pub enum AcpThreadEvent {
@@ -595,6 +596,7 @@ impl AcpThread {
         title: SharedString,
         child_status: Option<Task<Result<()>>>,
         project: Entity<Project>,
+        session_id: acp::SessionId,
         cx: &mut Context<Self>,
     ) -> Self {
         let action_log = cx.new(|_| ActionLog::new(project.clone()));
@@ -609,19 +611,7 @@ impl AcpThread {
             send_task: None,
             connection: Arc::new(connection),
             child_status,
-        }
-    }
-
-    /// 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)?)
+            session_id,
         }
     }
 
@@ -778,13 +768,13 @@ impl AcpThread {
     pub fn request_tool_call_permission(
         &mut self,
         tool_call: acp::ToolCall,
-        possible_grants: Vec<acp::Grant>,
+        options: Vec<acp::PermissionOption>,
         cx: &mut Context<Self>,
-    ) -> oneshot::Receiver<acp::GrantId> {
+    ) -> oneshot::Receiver<acp::PermissionOutcome> {
         let (tx, rx) = oneshot::channel();
 
         let status = ToolCallStatus::WaitingForConfirmation {
-            possible_grants,
+            options,
             respond_tx: tx,
         };
 
@@ -812,25 +802,32 @@ impl AcpThread {
     pub fn authorize_tool_call(
         &mut self,
         id: acp::ToolCallId,
-        grant: acp::Grant,
+        option: acp::PermissionOption,
         cx: &mut Context<Self>,
     ) {
         let Some((ix, call)) = self.tool_call_mut(&id) else {
             return;
         };
 
-        let new_status = if grant.is_allowed {
-            ToolCallStatus::Allowed {
-                status: acp::ToolCallStatus::InProgress,
+        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,
+                }
             }
-        } else {
-            ToolCallStatus::Rejected
         };
 
         let curr_status = mem::replace(&mut call.status, new_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) {
             panic!("tried to authorize an already authorized tool call");
         }
@@ -941,8 +938,11 @@ impl AcpThread {
         message: Vec<acp::ContentBlock>,
         cx: &mut Context<Self>,
     ) -> BoxFuture<'static, Result<(), acp_old::Error>> {
-        let block =
-            ContentBlock::new_combined(message.clone(), self.project.read(cx).languages(), cx);
+        let block = ContentBlock::new_combined(
+            message.clone(),
+            self.project.read(cx).languages().clone(),
+            cx,
+        );
         self.push_entry(
             AgentThreadEntry::UserMessage(UserMessage { content: block }),
             cx,
@@ -955,7 +955,14 @@ impl AcpThread {
             async {
                 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();
                 this.update(cx, |this, _cx| this.send_task.take())?;
                 anyhow::Ok(())
@@ -975,7 +982,7 @@ impl AcpThread {
 
     pub fn cancel(&mut self, cx: &mut Context<Self>) -> Task<Result<(), acp_old::Error>> {
         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| {
                 request.await?;
                 this.update(cx, |this, _cx| {
@@ -994,17 +1001,10 @@ impl AcpThread {
                                     mem::replace(&mut call.status, ToolCallStatus::Canceled);
 
                                 if let ToolCallStatus::WaitingForConfirmation {
-                                    respond_tx,
-                                    possible_grants,
+                                    respond_tx, ..
                                 } = curr_status
                                 {
-                                    if let Some(grant_id) = possible_grants
-                                        .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();
-                                    }
+                                    respond_tx.send(acp::PermissionOutcome::Canceled).ok();
                                 }
                             }
                         }
@@ -1171,12 +1171,17 @@ impl AcpThread {
 pub struct OldAcpClientDelegate {
     thread: 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: 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<()> {
@@ -1196,10 +1201,11 @@ impl OldAcpClientDelegate {
         confirmation: acp_old::ToolCallConfirmation,
     ) -> Result<acp_old::ToolCallConfirmationOutcome> {
         let cx = &mut self.cx.clone();
+
         let ToolCallRequest { outcome, .. } = cx
             .update(|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")??;
@@ -1264,10 +1270,26 @@ impl acp_old::Client for OldAcpClientDelegate {
         request: acp_old::PushToolCallParams,
     ) -> Result<acp_old::PushToolCallResponse, acp_old::Error> {
         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
             .update(|cx| {
                 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")?;
 

crates/acp_thread/src/connection.rs 🔗

@@ -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 futures::future::{FutureExt as _, LocalBoxFuture};
 
 pub trait AgentConnection {
-    fn request_any(
+    fn new_session(
         &self,
-        params: acp_old::AnyAgentRequest,
-    ) -> LocalBoxFuture<'static, Result<acp_old::AnyAgentResult>>;
+        params: acp::NewSessionToolArguments,
+    ) -> 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 {
-    fn request_any(
+    fn new_session(
         &self,
-        params: acp_old::AnyAgentRequest,
-    ) -> LocalBoxFuture<'static, Result<acp_old::AnyAgentResult>> {
-        let task = self.request_any(params);
-        async move { Ok(task.await?) }.boxed_local()
+        _params: acp::NewSessionToolArguments,
+    ) -> LocalBoxFuture<'static, Result<acp::SessionId>> {
+        let task = self.request_any(
+            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()
     }
 }

crates/agent_ui/src/acp/thread_view.rs 🔗

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