diff --git a/crates/acp_thread/src/acp_thread.rs b/crates/acp_thread/src/acp_thread.rs index 840c443b11a679cae349a33ffe0a2323445dceea..a5503e8b6b81d7e532c0ac076426e5498720f20c 100644 --- a/crates/acp_thread/src/acp_thread.rs +++ b/crates/acp_thread/src/acp_thread.rs @@ -932,7 +932,7 @@ impl TokenUsage { } } -#[derive(Debug, Clone, PartialEq, Eq)] +#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)] pub enum TokenUsageRatio { Normal, Warning, diff --git a/crates/agent_ui/src/acp/thread_view.rs b/crates/agent_ui/src/acp/thread_view.rs index ce17e9325cc3ed813593d02cbc087bc9515cf9c3..1a3deabda6a9c4350166d1d9aefd533be21f0d27 100644 --- a/crates/agent_ui/src/acp/thread_view.rs +++ b/crates/agent_ui/src/acp/thread_view.rs @@ -797,12 +797,13 @@ impl AcpServerView { } _ => {} } - if let Some(load_err) = err.downcast_ref::() { - self.server_state = ServerState::LoadError(load_err.clone()); + let load_error = if let Some(load_err) = err.downcast_ref::() { + load_err.clone() } else { - self.server_state = - ServerState::LoadError(LoadError::Other(format!("{:#}", err).into())) - } + LoadError::Other(format!("{:#}", err).into()) + }; + self.emit_load_error_telemetry(&load_error); + self.server_state = ServerState::LoadError(load_error); cx.notify(); } @@ -1069,6 +1070,7 @@ impl AcpServerView { } AcpThreadEvent::TokenUsageUpdated => { self.update_turn_tokens(cx); + self.emit_token_limit_telemetry_if_needed(thread, cx); } AcpThreadEvent::AvailableCommandsUpdated(available_commands) => { let mut available_commands = available_commands.clone(); @@ -1628,6 +1630,77 @@ impl AcpServerView { .into_any_element() } + fn emit_token_limit_telemetry_if_needed( + &mut self, + thread: &Entity, + cx: &mut Context, + ) { + let Some(active_thread) = self.as_active_thread() else { + return; + }; + + let (ratio, agent_telemetry_id, session_id) = { + let thread_data = thread.read(cx); + let Some(token_usage) = thread_data.token_usage() else { + return; + }; + ( + token_usage.ratio(), + thread_data.connection().telemetry_id(), + thread_data.session_id().clone(), + ) + }; + + let kind = match ratio { + acp_thread::TokenUsageRatio::Normal => { + active_thread.update(cx, |active, _cx| { + active.last_token_limit_telemetry = None; + }); + return; + } + acp_thread::TokenUsageRatio::Warning => "warning", + acp_thread::TokenUsageRatio::Exceeded => "exceeded", + }; + + let should_skip = active_thread + .read(cx) + .last_token_limit_telemetry + .as_ref() + .is_some_and(|last| *last >= ratio); + if should_skip { + return; + } + + active_thread.update(cx, |active, _cx| { + active.last_token_limit_telemetry = Some(ratio); + }); + + telemetry::event!( + "Agent Token Limit Warning", + agent = agent_telemetry_id, + session_id = session_id, + kind = kind, + ); + } + + fn emit_load_error_telemetry(&self, error: &LoadError) { + let error_kind = match error { + LoadError::Unsupported { .. } => "unsupported", + LoadError::FailedToInstall(_) => "failed_to_install", + LoadError::Exited { .. } => "exited", + LoadError::Other(_) => "other", + }; + + let agent_name = self.agent.name(); + + telemetry::event!( + "Agent Panel Error Shown", + agent = agent_name, + kind = error_kind, + message = error.to_string(), + ); + } + fn render_load_error( &self, e: &LoadError, diff --git a/crates/agent_ui/src/acp/thread_view/active_thread.rs b/crates/agent_ui/src/acp/thread_view/active_thread.rs index 6e30c6d2769c6df444b9aeaa35f2d935553f199b..1ab38529e688bef0cd1960f10bf37f095db017f1 100644 --- a/crates/agent_ui/src/acp/thread_view/active_thread.rs +++ b/crates/agent_ui/src/acp/thread_view/active_thread.rs @@ -180,6 +180,7 @@ pub struct AcpThreadView { pub(super) thread_error: Option, pub thread_error_markdown: Option>, pub token_limit_callout_dismissed: bool, + pub last_token_limit_telemetry: Option, thread_feedback: ThreadFeedbackState, pub list_state: ListState, pub prompt_capabilities: Rc>, @@ -356,6 +357,7 @@ impl AcpThreadView { thread_error: None, thread_error_markdown: None, token_limit_callout_dismissed: false, + last_token_limit_telemetry: None, thread_feedback: Default::default(), expanded_tool_calls: HashSet::default(), expanded_tool_call_raw_inputs: HashSet::default(), diff --git a/crates/agent_ui/src/agent_panel.rs b/crates/agent_ui/src/agent_panel.rs index 8f540cd1dee8582b40c2a72550c37f6cd87463c0..8dd3ba19f79bcbc41fdccd3d16fd8b4368afc18c 100644 --- a/crates/agent_ui/src/agent_panel.rs +++ b/crates/agent_ui/src/agent_panel.rs @@ -440,6 +440,7 @@ pub struct AgentPanel { onboarding: Entity, selected_agent: AgentType, show_trust_workspace_message: bool, + last_configuration_error_telemetry: Option, } impl AgentPanel { @@ -660,6 +661,7 @@ impl AgentPanel { thread_store, selected_agent: AgentType::default(), show_trust_workspace_message: false, + last_configuration_error_telemetry: None, }; // Initial sync of agent servers from extensions @@ -2694,6 +2696,33 @@ impl AgentPanel { ) } + fn emit_configuration_error_telemetry_if_needed( + &mut self, + configuration_error: Option<&ConfigurationError>, + ) { + let error_kind = configuration_error.map(|err| match err { + ConfigurationError::NoProvider => "no_provider", + ConfigurationError::ModelNotFound => "model_not_found", + ConfigurationError::ProviderNotAuthenticated(_) => "provider_not_authenticated", + }); + + let error_kind_string = error_kind.map(String::from); + + if self.last_configuration_error_telemetry == error_kind_string { + return; + } + + self.last_configuration_error_telemetry = error_kind_string; + + if let Some(kind) = error_kind { + let message = configuration_error + .map(|err| err.to_string()) + .unwrap_or_default(); + + telemetry::event!("Agent Panel Error Shown", kind = kind, message = message,); + } + } + fn render_configuration_error( &self, border_bottom: bool, @@ -2978,46 +3007,57 @@ impl Render for AgentPanel { .child(self.render_toolbar(window, cx)) .children(self.render_workspace_trust_message(cx)) .children(self.render_onboarding(window, cx)) - .map(|parent| match &self.active_view { - ActiveView::Uninitialized => parent, - ActiveView::AgentThread { thread_view, .. } => parent - .child(thread_view.clone()) - .child(self.render_drag_target(cx)), - ActiveView::History { kind } => match kind { - HistoryKind::AgentThreads => parent.child(self.acp_history.clone()), - HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()), - }, - ActiveView::TextThread { - text_thread_editor, - buffer_search_bar, - .. - } => { + .map(|parent| { + // Emit configuration error telemetry before entering the match to avoid borrow conflicts + if matches!(&self.active_view, ActiveView::TextThread { .. }) { let model_registry = LanguageModelRegistry::read_global(cx); let configuration_error = model_registry.configuration_error(model_registry.default_model(), cx); - parent - .map(|this| { - if !self.should_render_onboarding(cx) - && let Some(err) = configuration_error.as_ref() - { - this.child(self.render_configuration_error( - true, - err, - &self.focus_handle(cx), - cx, - )) - } else { - this - } - }) - .child(self.render_text_thread( - text_thread_editor, - buffer_search_bar, - window, - cx, - )) + self.emit_configuration_error_telemetry_if_needed(configuration_error.as_ref()); + } + + match &self.active_view { + ActiveView::Uninitialized => parent, + ActiveView::AgentThread { thread_view, .. } => parent + .child(thread_view.clone()) + .child(self.render_drag_target(cx)), + ActiveView::History { kind } => match kind { + HistoryKind::AgentThreads => parent.child(self.acp_history.clone()), + HistoryKind::TextThreads => parent.child(self.text_thread_history.clone()), + }, + ActiveView::TextThread { + text_thread_editor, + buffer_search_bar, + .. + } => { + let model_registry = LanguageModelRegistry::read_global(cx); + let configuration_error = + model_registry.configuration_error(model_registry.default_model(), cx); + + parent + .map(|this| { + if !self.should_render_onboarding(cx) + && let Some(err) = configuration_error.as_ref() + { + this.child(self.render_configuration_error( + true, + err, + &self.focus_handle(cx), + cx, + )) + } else { + this + } + }) + .child(self.render_text_thread( + text_thread_editor, + buffer_search_bar, + window, + cx, + )) + } + ActiveView::Configuration => parent.children(self.configuration.clone()), } - ActiveView::Configuration => parent.children(self.configuration.clone()), }) .children(self.render_trial_end_upsell(window, cx));