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,
Katie Geer , Michael Benfield , and Claude Opus 4.5 created
This PR extends error telemetry coverage in the agent panel, building on
#46836 which added telemetry for thread errors.
`"Agent Panel Error Shown"` (extended)
Now also fires for:
- **Load errors** - when an agent fails to start
- **Configuration errors** - when LLM provider setup is incomplete
`"Agent Token Limit Warning"` (new event)
Fires when a thread approaches or exceeds its token/context limit. This
is tracked separately from errors because it's an informational warning,
not a failure.
## Implementation Notes
- All telemetry fires **once per error occurrence**, not on every render
- Load error telemetry fires in `handle_load_error()` when the error is
first received
- Token limit telemetry uses a flag to track state transitions (Normal →
Warning → Exceeded)
- Configuration error telemetry tracks the last emitted error kind to
avoid duplicates
Release Notes:
- N/A *or* Added/Fixed/Improved ...
---------
Co-authored-by: Michael Benfield <mbenfield@zed.dev>
Co-authored-by: Claude Opus 4.5 <noreply@anthropic.com>
crates/acp_thread/src/acp_thread.rs | 2
crates/agent_ui/src/acp/thread_view.rs | 83 +++++++++
crates/agent_ui/src/acp/thread_view/active_thread.rs | 2
crates/agent_ui/src/agent_panel.rs | 112 +++++++++----
4 files changed, 157 insertions(+), 42 deletions(-)
@@ -932,7 +932,7 @@ impl TokenUsage {
}
}
-#[derive(Debug, Clone, PartialEq, Eq)]
+#[derive(Debug, Clone, PartialEq, Eq, PartialOrd, Ord)]
pub enum TokenUsageRatio {
Normal,
Warning,
@@ -797,12 +797,13 @@ impl AcpServerView {
}
_ => {}
}
- if let Some(load_err) = err.downcast_ref::<LoadError>() {
- self.server_state = ServerState::LoadError(load_err.clone());
+ let load_error = if let Some(load_err) = err.downcast_ref::<LoadError>() {
+ 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<AcpThread>,
+ cx: &mut Context<Self>,
+ ) {
+ 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,
@@ -180,6 +180,7 @@ pub struct AcpThreadView {
pub(super) thread_error: Option<ThreadError>,
pub thread_error_markdown: Option<Entity<Markdown>>,
pub token_limit_callout_dismissed: bool,
+ pub last_token_limit_telemetry: Option<acp_thread::TokenUsageRatio>,
thread_feedback: ThreadFeedbackState,
pub list_state: ListState,
pub prompt_capabilities: Rc<RefCell<PromptCapabilities>>,
@@ -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(),
@@ -440,6 +440,7 @@ pub struct AgentPanel {
onboarding: Entity<AgentPanelOnboarding>,
selected_agent: AgentType,
show_trust_workspace_message: bool,
+ last_configuration_error_telemetry: Option<String>,
}
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));