@@ -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<bool>,
},
// todo! Running?
- Allowed,
+ Allowed {
+ // todo! should this be variants in crate::ToolCallStatus instead?
+ status: acp::ToolCallStatus,
+ content: Option<Entity<Markdown>>,
+ },
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<acp::ToolCallContent>,
+ cx: &mut Context<Self>,
+ ) -> 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 { .. },
..
})
));
@@ -202,6 +202,27 @@ impl acp::Client for AcpClientDelegate {
Ok(acp::RequestToolCallResponse::Rejected)
}
}
+
+ async fn update_tool_call(
+ &self,
+ request: acp::UpdateToolCallParams,
+ ) -> Result<acp::UpdateToolCallResponse> {
+ 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 {
@@ -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<Self>) -> 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,
};