From 3d7fe6e1f815b49af2d6db21ce1d1c9a4b750bd1 Mon Sep 17 00:00:00 2001 From: Lukas Wirth Date: Fri, 30 Jan 2026 19:07:08 +0100 Subject: [PATCH] agent_ui: Add debug command to copy and paste agent threads (#48039) Temporary feature meant to aid with internal performance debugging Release Notes: - N/A *or* Added/Fixed/Improved ... --- crates/agent_ui/Cargo.toml | 2 +- crates/agent_ui/src/agent_panel.rs | 203 ++++++++++++++++++++++++++++- crates/agent_ui/src/agent_ui.rs | 4 + 3 files changed, 201 insertions(+), 8 deletions(-) diff --git a/crates/agent_ui/Cargo.toml b/crates/agent_ui/Cargo.toml index 289e66164d5012dcb5ce6e69d73774b5cca04b76..502352cbf1cafbabd02c93887f19f478c79965fa 100644 --- a/crates/agent_ui/Cargo.toml +++ b/crates/agent_ui/Cargo.toml @@ -40,6 +40,7 @@ assistant_text_thread.workspace = true assistant_slash_command.workspace = true assistant_slash_commands.workspace = true audio.workspace = true +base64.workspace = true buffer_diff.workspace = true chrono.workspace = true client.workspace = true @@ -118,7 +119,6 @@ reqwest_client = { workspace = true, optional = true } [dev-dependencies] acp_thread = { workspace = true, features = ["test-support"] } -base64.workspace = true agent = { workspace = true, features = ["test-support"] } assistant_text_thread = { workspace = true, features = ["test-support"] } buffer_diff = { workspace = true, features = ["test-support"] } diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 9ed119d5a63e7a3687304cd4c79bb7294c931f92..15009c3d929bb8926499aafc7dbcd0601135152c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -1,7 +1,8 @@ use std::{ops::Range, path::Path, rc::Rc, sync::Arc, time::Duration}; use acp_thread::{AcpThread, AgentSessionInfo}; -use agent::{ContextServerRegistry, ThreadStore}; +use agent::{ContextServerRegistry, SharedThread, ThreadStore}; +use agent_client_protocol as acp; use agent_servers::AgentServer; use db::kvp::{Dismissable, KEY_VALUE_STORE}; use project::{ @@ -16,9 +17,10 @@ use zed_actions::agent::{OpenClaudeCodeOnboardingModal, ReauthenticateAgent}; use crate::ManageProfiles; use crate::ui::{AcpOnboardingModal, ClaudeCodeOnboardingModal}; use crate::{ - AddContextServer, AgentDiffPane, Follow, InlineAssistant, NewTextThread, NewThread, - OpenActiveThreadAsMarkdown, OpenAgentDiff, OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, - ToggleNavigationMenu, ToggleNewThreadMenu, ToggleOptionsMenu, + AddContextServer, AgentDiffPane, CopyThreadToClipboard, Follow, InlineAssistant, + LoadThreadFromClipboard, NewTextThread, NewThread, OpenActiveThreadAsMarkdown, OpenAgentDiff, + OpenHistory, ResetTrialEndUpsell, ResetTrialUpsell, ToggleNavigationMenu, ToggleNewThreadMenu, + ToggleOptionsMenu, acp::AcpThreadView, agent_configuration::{AgentConfiguration, AssistantConfigurationEvent}, slash_command::SlashCommandCompletionProvider, @@ -46,9 +48,9 @@ use extension::ExtensionEvents; use extension_host::ExtensionStore; use fs::Fs; use gpui::{ - Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, Corner, DismissEvent, - Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, Subscription, - Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, + Action, Animation, AnimationExt, AnyElement, App, AsyncWindowContext, ClipboardItem, Corner, + DismissEvent, Entity, EventEmitter, ExternalPaths, FocusHandle, Focusable, KeyContext, Pixels, + Subscription, Task, UpdateGlobal, WeakEntity, prelude::*, pulsating_between, }; use language::LanguageRegistry; use language_model::{ConfigurationError, LanguageModelRegistry}; @@ -212,6 +214,21 @@ pub fn init(cx: &mut App) { panel.reset_agent_zoom(window, cx); }); } + }) + .register_action(|workspace, _: &CopyThreadToClipboard, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + panel.update(cx, |panel, cx| { + panel.copy_thread_to_clipboard(window, cx); + }); + } + }) + .register_action(|workspace, _: &LoadThreadFromClipboard, window, cx| { + if let Some(panel) = workspace.panel::(cx) { + workspace.focus_panel::(window, cx); + panel.update(cx, |panel, cx| { + panel.load_thread_from_clipboard(window, cx); + }); + } }); }, ) @@ -1187,6 +1204,178 @@ impl AgentPanel { } } + fn copy_thread_to_clipboard(&mut self, window: &mut Window, cx: &mut Context) { + let Some(thread) = self.active_native_agent_thread(cx) else { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + struct NoThreadToast; + workspace.show_toast( + workspace::Toast::new( + workspace::notifications::NotificationId::unique::(), + "No active native thread to copy", + ) + .autohide(), + cx, + ); + }); + } + return; + }; + + let workspace = self.workspace.clone(); + let load_task = thread.read(cx).to_db(cx); + + cx.spawn_in(window, async move |_this, cx| { + let db_thread = load_task.await; + let shared_thread = SharedThread::from_db_thread(&db_thread); + let thread_data = shared_thread.to_bytes()?; + let encoded = base64::Engine::encode(&base64::prelude::BASE64_STANDARD, &thread_data); + + cx.update(|_window, cx| { + cx.write_to_clipboard(ClipboardItem::new_string(encoded)); + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + struct ThreadCopiedToast; + workspace.show_toast( + workspace::Toast::new( + workspace::notifications::NotificationId::unique::(), + "Thread copied to clipboard (base64 encoded)", + ) + .autohide(), + cx, + ); + }); + } + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + + fn load_thread_from_clipboard(&mut self, window: &mut Window, cx: &mut Context) { + let Some(clipboard) = cx.read_from_clipboard() else { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + struct NoClipboardToast; + workspace.show_toast( + workspace::Toast::new( + workspace::notifications::NotificationId::unique::(), + "No clipboard content available", + ) + .autohide(), + cx, + ); + }); + } + return; + }; + + let Some(encoded) = clipboard.text() else { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + struct InvalidClipboardToast; + workspace.show_toast( + workspace::Toast::new( + workspace::notifications::NotificationId::unique::(), + "Clipboard does not contain text", + ) + .autohide(), + cx, + ); + }); + } + return; + }; + + let thread_data = match base64::Engine::decode(&base64::prelude::BASE64_STANDARD, &encoded) + { + Ok(data) => data, + Err(_) => { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + struct DecodeErrorToast; + workspace.show_toast( + workspace::Toast::new( + workspace::notifications::NotificationId::unique::(), + "Failed to decode clipboard content (expected base64)", + ) + .autohide(), + cx, + ); + }); + } + return; + } + }; + + let shared_thread = match SharedThread::from_bytes(&thread_data) { + Ok(thread) => thread, + Err(_) => { + if let Some(workspace) = self.workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + struct ParseErrorToast; + workspace.show_toast( + workspace::Toast::new( + workspace::notifications::NotificationId::unique::( + ), + "Failed to parse thread data from clipboard", + ) + .autohide(), + cx, + ); + }); + } + return; + } + }; + + let db_thread = shared_thread.to_db_thread(); + let session_id = acp::SessionId::new(uuid::Uuid::new_v4().to_string()); + let thread_store = self.thread_store.clone(); + let title = db_thread.title.clone(); + let workspace = self.workspace.clone(); + + cx.spawn_in(window, async move |this, cx| { + thread_store + .update(&mut cx.clone(), |store, cx| { + store.save_thread(session_id.clone(), db_thread, cx) + }) + .await?; + + let thread_metadata = acp_thread::AgentSessionInfo { + session_id, + cwd: None, + title: Some(title), + updated_at: Some(chrono::Utc::now()), + meta: None, + }; + + this.update_in(cx, |this, window, cx| { + this.open_thread(thread_metadata, window, cx); + })?; + + this.update_in(cx, |_, _window, cx| { + if let Some(workspace) = workspace.upgrade() { + workspace.update(cx, |workspace, cx| { + struct ThreadLoadedToast; + workspace.show_toast( + workspace::Toast::new( + workspace::notifications::NotificationId::unique::(), + "Thread loaded from clipboard", + ) + .autohide(), + cx, + ); + }); + } + })?; + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); + } + fn handle_agent_configuration_event( &mut self, _entity: &Entity, diff --git a/crates/agent_ui/src/agent_ui.rs b/crates/agent_ui/src/agent_ui.rs index 10b8e9f0d61fa4b4ea95e0618c14c551d041787c..b0ea43d041da6b0888a15ec8f7d46520ae3d5606 100644 --- a/crates/agent_ui/src/agent_ui.rs +++ b/crates/agent_ui/src/agent_ui.rs @@ -101,6 +101,10 @@ actions!( OpenActiveThreadAsMarkdown, /// Opens the agent diff view to review changes. OpenAgentDiff, + /// Copies the current thread to the clipboard as JSON for debugging. + CopyThreadToClipboard, + /// Loads a thread from the clipboard JSON for debugging. + LoadThreadFromClipboard, /// Keeps the current suggestion or change. Keep, /// Rejects the current suggestion or change.