From 0623642dbebc8bb4fac64d1845ebd2bf2b112b8c Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Thu, 22 Jan 2026 20:44:26 +0000 Subject: [PATCH] Add "Agent Panel Error Shown" telemetry with ACP error details (#46848) (cherry-pick to preview) (#47428) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of #46848 to preview ---- Adds a new "Agent Panel Error Shown" telemetry event that fires when users see errors in the agent panel. For errors from external ACP agents (like Claude Code), we capture additional details. Previously, we had no visibility into what errors users were encountering in the agent panel. This made it difficult to diagnose issues, especially with external agents. The new telemetry event includes: - `agent` — The agent telemetry ID - `session_id` — The session ID - `kind` — Error category (payment_required, authentication_required, refusal, other, etc.) - `acp_error_code` — The ACP error code when available (e.g., "InternalError") - `acp_error_message` — The ACP error message when available Release Notes: - N/A --------- Co-authored-by: Michael Benfield Co-authored-by: Katie Geer Co-authored-by: Michael Benfield --- crates/agent_ui/src/acp/thread_view.rs | 85 +++++++++++++++++++++++--- 1 file changed, 75 insertions(+), 10 deletions(-) diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7910059f4e300d27d9910a7b1a0827ab55dec616..80b2fc649706ee90cab2c8464c82266fc9faa698 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -91,7 +91,10 @@ enum ThreadError { PaymentRequired, Refusal, AuthenticationRequired(SharedString), - Other(SharedString), + Other { + message: SharedString, + acp_error_code: Option, + }, } impl ThreadError { @@ -103,16 +106,25 @@ impl ThreadError { { Self::AuthenticationRequired(acp_error.message.clone().into()) } else { - let string = format!("{:#}", error); + let message: SharedString = format!("{:#}", error).into(); + + // Extract ACP error code if available + let acp_error_code = error + .downcast_ref::() + .map(|acp_error| SharedString::from(acp_error.code.to_string())); + // TODO: we should have Gemini return better errors here. if agent.clone().downcast::().is_some() - && string.contains("Could not load the default credentials") - || string.contains("API key not valid") - || string.contains("Request had invalid authentication credentials") + && message.contains("Could not load the default credentials") + || message.contains("API key not valid") + || message.contains("Request had invalid authentication credentials") { - Self::AuthenticationRequired(string.into()) + Self::AuthenticationRequired(message) } else { - Self::Other(string.into()) + Self::Other { + message, + acp_error_code, + } } } } @@ -1856,10 +1868,59 @@ impl AcpThreadView { } fn handle_thread_error(&mut self, error: anyhow::Error, cx: &mut Context) { - self.thread_error = Some(ThreadError::from_err(error, &self.agent)); + let thread_error = ThreadError::from_err(error, &self.agent); + self.emit_thread_error_telemetry(&thread_error, cx); + self.thread_error = Some(thread_error); cx.notify(); } + fn emit_thread_error_telemetry(&self, error: &ThreadError, cx: &mut Context) { + let (error_kind, acp_error_code, message): (&str, Option, SharedString) = + match error { + ThreadError::PaymentRequired => ( + "payment_required", + None, + "You reached your free usage limit. Upgrade to Zed Pro for more prompts." + .into(), + ), + ThreadError::Refusal => { + let model_or_agent_name = self.current_model_name(cx); + let message = format!( + "{} refused to respond to this prompt. This can happen when a model believes the prompt violates its content policy or safety guidelines, so rephrasing it can sometimes address the issue.", + model_or_agent_name + ); + ("refusal", None, message.into()) + } + ThreadError::AuthenticationRequired(message) => { + ("authentication_required", None, message.clone()) + } + ThreadError::Other { + acp_error_code, + message, + } => ("other", acp_error_code.clone(), message.clone()), + }; + + let (agent_telemetry_id, session_id) = self + .thread() + .map(|t| { + let thread = t.read(cx); + ( + thread.connection().telemetry_id(), + thread.session_id().clone(), + ) + }) + .unzip(); + + telemetry::event!( + "Agent Panel Error Shown", + agent = agent_telemetry_id, + session_id = session_id, + kind = error_kind, + acp_error_code = acp_error_code, + message = message, + ); + } + fn clear_thread_error(&mut self, cx: &mut Context) { self.thread_error = None; self.thread_error_markdown = None; @@ -1942,7 +2003,9 @@ impl AcpThreadView { } AcpThreadEvent::Refusal => { self.thread_retry_status.take(); - self.thread_error = Some(ThreadError::Refusal); + let thread_error = ThreadError::Refusal; + self.emit_thread_error_telemetry(&thread_error, cx); + self.thread_error = Some(thread_error); let model_or_agent_name = self.current_model_name(cx); let notification_message = format!("{} refused to respond to this request", model_or_agent_name); @@ -7617,7 +7680,9 @@ impl AcpThreadView { fn render_thread_error(&mut self, window: &mut Window, cx: &mut Context) -> Option
{ let content = match self.thread_error.as_ref()? { - ThreadError::Other(error) => self.render_any_thread_error(error.clone(), window, cx), + ThreadError::Other { message, .. } => { + self.render_any_thread_error(message.clone(), window, cx) + } ThreadError::Refusal => self.render_refusal_error(cx), ThreadError::AuthenticationRequired(error) => { self.render_authentication_required_error(error.clone(), cx)