diff --git a/crates/acp/src/acp.rs b/crates/acp/src/acp.rs index 1fc225ee7ca771d1f1ca1fe4719c3cce16b023e5..403915ec3c8f456a5d326c126940009a3815d380 100644 --- a/crates/acp/src/acp.rs +++ b/crates/acp/src/acp.rs @@ -2,7 +2,7 @@ mod server; mod thread_view; use agentic_coding_protocol::{self as acp, Role}; -use anyhow::Result; +use anyhow::{Context as _, Result}; use chrono::{DateTime, Utc}; use futures::channel::oneshot; use gpui::{AppContext, Context, Entity, EventEmitter, SharedString, Task}; @@ -134,7 +134,11 @@ pub enum ToolCallStatus { respond_tx: oneshot::Sender, }, // todo! Running? - Allowed, + Allowed { + // todo! should this be variants in crate::ToolCallStatus instead? + status: acp::ToolCallStatus, + content: Option>, + }, Rejected, } @@ -310,7 +314,10 @@ impl AcpThread { }; let new_status = if allowed { - ToolCallStatus::Allowed + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Running, + content: None, + } } else { ToolCallStatus::Rejected }; @@ -326,6 +333,43 @@ impl AcpThread { cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize)); } + pub fn update_tool_call( + &mut self, + id: ToolCallId, + new_status: acp::ToolCallStatus, + new_content: Option, + cx: &mut Context, + ) -> Result<()> { + let language_registry = self.project.read(cx).languages().clone(); + let entry = self.entry_mut(id.0).context("Entry not found")?; + + match &mut entry.content { + AgentThreadEntryContent::ToolCall(call) => match &mut call.status { + ToolCallStatus::Allowed { content, status } => { + *content = new_content.map(|new_content| { + let acp::ToolCallContent::Markdown { markdown } = new_content; + + cx.new(|cx| { + Markdown::new(markdown.into(), Some(language_registry), None, cx) + }) + }); + + *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") + } + }, + _ => anyhow::bail!("Entry is not a tool call"), + } + + cx.emit(AcpThreadEvent::EntryUpdated(id.as_u64() as usize)); + Ok(()) + } + fn entry_mut(&mut self, id: ThreadEntryId) -> Option<&mut ThreadEntry> { let entry = self.entries.get_mut(id.0 as usize); debug_assert!( @@ -341,7 +385,7 @@ impl AcpThread { match &entry.content { AgentThreadEntryContent::ToolCall(call) => match call.status { ToolCallStatus::WaitingForConfirmation { .. } => return true, - ToolCallStatus::Allowed | ToolCallStatus::Rejected => continue, + ToolCallStatus::Allowed { .. } | ToolCallStatus::Rejected => continue, }, AgentThreadEntryContent::Message(_) => { // Reached the beginning of the turn @@ -478,7 +522,7 @@ mod tests { assert!(matches!( thread.entries().last().unwrap().content, AgentThreadEntryContent::ToolCall(ToolCall { - status: ToolCallStatus::Allowed, + status: ToolCallStatus::Allowed { .. }, .. }) )); @@ -498,7 +542,7 @@ mod tests { assert!(matches!( thread.entries[1].content, AgentThreadEntryContent::ToolCall(ToolCall { - status: ToolCallStatus::Allowed, + status: ToolCallStatus::Allowed { .. }, .. }) )); diff --git a/crates/acp/src/server.rs b/crates/acp/src/server.rs index b485e6a3463b93d068124164358c8968c82c7afe..f64cde1cfd5a22469dae92c0c1ba8270b88f92c5 100644 --- a/crates/acp/src/server.rs +++ b/crates/acp/src/server.rs @@ -202,6 +202,27 @@ impl acp::Client for AcpClientDelegate { Ok(acp::RequestToolCallResponse::Rejected) } } + + async fn update_tool_call( + &self, + request: acp::UpdateToolCallParams, + ) -> Result { + let cx = &mut self.cx.clone(); + + cx.update(|cx| { + self.update_thread(&request.thread_id.into(), cx, |thread, cx| { + thread.update_tool_call( + request.tool_call_id.into(), + request.status, + request.content, + cx, + ) + }) + })? + .context("Failed to update thread")??; + + Ok(acp::UpdateToolCallResponse) + } } impl AcpServer { diff --git a/crates/acp/src/thread_view.rs b/crates/acp/src/thread_view.rs index a6390d62ac17a36ec6e81d0008dbc40606e74900..f32218a3fe6bbdf09a86da0e403776350fa4445f 100644 --- a/crates/acp/src/thread_view.rs +++ b/crates/acp/src/thread_view.rs @@ -1,11 +1,14 @@ use std::path::Path; use std::rc::Rc; +use std::time::Duration; +use agentic_coding_protocol::{self as acp}; use anyhow::Result; use editor::{Editor, MultiBuffer}; use gpui::{ - App, EdgesRefinement, Empty, Entity, Focusable, ListState, SharedString, StyleRefinement, - Subscription, TextStyleRefinement, UnderlineStyle, Window, div, list, prelude::*, + Animation, AnimationExt, App, EdgesRefinement, Empty, Entity, Focusable, ListState, + SharedString, StyleRefinement, Subscription, TextStyleRefinement, Transformation, + UnderlineStyle, Window, div, list, percentage, prelude::*, }; use gpui::{FocusHandle, Task}; use language::Buffer; @@ -256,11 +259,30 @@ impl AcpThreadView { fn render_tool_call(&self, tool_call: &ToolCall, window: &Window, cx: &Context) -> Div { let status_icon = match &tool_call.status { ToolCallStatus::WaitingForConfirmation { .. } => Empty.into_element().into_any(), - ToolCallStatus::Allowed => Icon::new(IconName::Check) + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Running, + .. + } => Icon::new(IconName::ArrowCircle) + .color(Color::Success) + .size(IconSize::Small) + .with_animation( + "running", + Animation::new(Duration::from_secs(2)).repeat(), + |icon, delta| icon.transform(Transformation::rotate(percentage(delta))), + ) + .into_any_element(), + ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Finished, + .. + } => Icon::new(IconName::Check) .color(Color::Success) .size(IconSize::Small) .into_any_element(), - ToolCallStatus::Rejected => Icon::new(IconName::X) + ToolCallStatus::Rejected + | ToolCallStatus::Allowed { + status: acp::ToolCallStatus::Error, + .. + } => Icon::new(IconName::X) .color(Color::Error) .size(IconSize::Small) .into_any_element(), @@ -309,7 +331,18 @@ impl AcpThreadView { ) .into_any() .into(), - ToolCallStatus::Allowed => None, + ToolCallStatus::Allowed { content, .. } => content.clone().map(|content| { + div() + .border_color(cx.theme().colors().border) + .border_t_1() + .px_2() + .py_1p5() + .child(MarkdownElement::new( + content, + default_markdown_style(window, cx), + )) + .into_any_element() + }), ToolCallStatus::Rejected => None, };