diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index 7c204e9b0c4122306c20161446fccb6ad0e7a66d..3f03ecf32257fb2c5650de0289e37ac3a6efe139 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -99,7 +99,10 @@ enum ThreadError { PaymentRequired, Refusal, AuthenticationRequired(SharedString), - Other(SharedString), + Other { + message: SharedString, + acp_error_code: Option, + }, } impl ThreadError { @@ -111,16 +114,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, + } } } } @@ -1932,10 +1944,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; @@ -2018,7 +2079,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); @@ -7844,7 +7907,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)