From 8c7fb26af05556990d4019b14b73df32b254c0e5 Mon Sep 17 00:00:00 2001 From: Yordis Prieto Date: Tue, 14 Oct 2025 11:10:45 -0400 Subject: [PATCH] acp tools: Add button to copy all observed messages (#40076) Added a "Copy All Messages" button to the ACP logs toolbar that copies all messages in the watched stream to the clipboard as structured JSON. ## Motivation When troubleshooting ACP protocol implementations, it's helpful to provide the entire message thread to an LLM for analysis. Previously, I had to copy individual messages one at a time, which was tedious and time-consuming. This feature allows copying the entire conversation history in a single click. Release Notes: - Added: Copy All Messages button to ACP logs view --------- Signed-off-by: Yordis Prieto Co-authored-by: Agus Zubiaga --- crates/acp_tools/src/acp_tools.rs | 129 +++++++++++++++++++++++++++++- crates/zed/src/zed.rs | 2 + 2 files changed, 127 insertions(+), 4 deletions(-) diff --git a/crates/acp_tools/src/acp_tools.rs b/crates/acp_tools/src/acp_tools.rs index e20a040e9da70a40066f3e5534171818de34a936..7ba4f555a2a42303f82cfdc1f8e24860ed3e1d69 100644 --- a/crates/acp_tools/src/acp_tools.rs +++ b/crates/acp_tools/src/acp_tools.rs @@ -4,22 +4,26 @@ use std::{ fmt::Display, rc::{Rc, Weak}, sync::Arc, + time::Duration, }; use agent_client_protocol as acp; use collections::HashMap; use gpui::{ - App, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, ListState, - StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, prelude::*, + App, ClipboardItem, Empty, Entity, EventEmitter, FocusHandle, Focusable, Global, ListAlignment, + ListState, StyleRefinement, Subscription, Task, TextStyleRefinement, Window, actions, list, + prelude::*, }; use language::LanguageRegistry; use markdown::{CodeBlockRenderer, Markdown, MarkdownElement, MarkdownStyle}; use project::Project; use settings::Settings; use theme::ThemeSettings; -use ui::prelude::*; +use ui::{Tooltip, prelude::*}; use util::ResultExt as _; -use workspace::{Item, Workspace}; +use workspace::{ + Item, ItemHandle, ToolbarItemEvent, ToolbarItemLocation, ToolbarItemView, Workspace, +}; actions!(dev, [OpenAcpLogs]); @@ -227,6 +231,34 @@ impl AcpTools { cx.notify(); } + fn serialize_observed_messages(&self) -> Option { + let connection = self.watched_connection.as_ref()?; + + let messages: Vec = connection + .messages + .iter() + .filter_map(|message| { + let params = match &message.params { + Ok(Some(params)) => params.clone(), + Ok(None) => serde_json::Value::Null, + Err(err) => serde_json::to_value(err).ok()?, + }; + Some(serde_json::json!({ + "_direction": match message.direction { + acp::StreamMessageDirection::Incoming => "incoming", + acp::StreamMessageDirection::Outgoing => "outgoing", + }, + "_type": message.message_type.to_string().to_lowercase(), + "id": message.request_id, + "method": message.name.to_string(), + "params": params, + })) + }) + .collect(); + + serde_json::to_string_pretty(&messages).ok() + } + fn render_message( &mut self, index: usize, @@ -492,3 +524,92 @@ impl Render for AcpTools { }) } } + +pub struct AcpToolsToolbarItemView { + acp_tools: Option>, + just_copied: bool, +} + +impl AcpToolsToolbarItemView { + pub fn new() -> Self { + Self { + acp_tools: None, + just_copied: false, + } + } +} + +impl Render for AcpToolsToolbarItemView { + fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { + let Some(acp_tools) = self.acp_tools.as_ref() else { + return Empty.into_any_element(); + }; + + let acp_tools = acp_tools.clone(); + + h_flex() + .gap_2() + .child( + IconButton::new( + "copy_all_messages", + if self.just_copied { + IconName::Check + } else { + IconName::Copy + }, + ) + .icon_size(IconSize::Small) + .tooltip(Tooltip::text(if self.just_copied { + "Copied!" + } else { + "Copy All Messages" + })) + .disabled( + acp_tools + .read(cx) + .watched_connection + .as_ref() + .is_none_or(|connection| connection.messages.is_empty()), + ) + .on_click(cx.listener(move |this, _, _window, cx| { + if let Some(content) = acp_tools.read(cx).serialize_observed_messages() { + cx.write_to_clipboard(ClipboardItem::new_string(content)); + + this.just_copied = true; + cx.spawn(async move |this, cx| { + cx.background_executor().timer(Duration::from_secs(2)).await; + this.update(cx, |this, cx| { + this.just_copied = false; + cx.notify(); + }) + }) + .detach(); + } + })), + ) + .into_any() + } +} + +impl EventEmitter for AcpToolsToolbarItemView {} + +impl ToolbarItemView for AcpToolsToolbarItemView { + fn set_active_pane_item( + &mut self, + active_pane_item: Option<&dyn ItemHandle>, + _window: &mut Window, + cx: &mut Context, + ) -> ToolbarItemLocation { + if let Some(item) = active_pane_item + && let Some(acp_tools) = item.downcast::() + { + self.acp_tools = Some(acp_tools); + cx.notify(); + return ToolbarItemLocation::PrimaryRight; + } + if self.acp_tools.take().is_some() { + cx.notify(); + } + ToolbarItemLocation::Hidden + } +} diff --git a/crates/zed/src/zed.rs b/crates/zed/src/zed.rs index 928ffa5bafb7f45cd691011052622c0273acfd57..4a57939c407ee21bed48e12f7af2c44c6d51a1db 100644 --- a/crates/zed/src/zed.rs +++ b/crates/zed/src/zed.rs @@ -1041,6 +1041,8 @@ fn initialize_pane( toolbar.add_item(lsp_log_item, window, cx); let dap_log_item = cx.new(|_| debugger_tools::DapLogToolbarItemView::new()); toolbar.add_item(dap_log_item, window, cx); + let acp_tools_item = cx.new(|_| acp_tools::AcpToolsToolbarItemView::new()); + toolbar.add_item(acp_tools_item, window, cx); let syntax_tree_item = cx.new(|_| language_tools::SyntaxTreeToolbarItemView::new()); toolbar.add_item(syntax_tree_item, window, cx); let migration_banner = cx.new(|cx| MigrationBanner::new(workspace, cx));