Add agent metrics (#41991)

Bennet Bo Fenner created

Release Notes:

- N/A

Change summary

Cargo.lock                                        |   2 
crates/acp_thread/Cargo.toml                      |   1 
crates/acp_thread/src/acp_thread.rs               |  32 ++
crates/acp_thread/src/connection.rs               |   9 
crates/action_log/Cargo.toml                      |   1 
crates/action_log/src/action_log.rs               | 149 +++++++++++--
crates/agent/src/agent.rs                         |   8 
crates/agent_servers/src/acp.rs                   |   9 
crates/agent_servers/src/claude.rs                |   2 
crates/agent_servers/src/codex.rs                 |   2 
crates/agent_servers/src/custom.rs                |   2 
crates/agent_servers/src/gemini.rs                |   2 
crates/agent_ui/src/acp/model_selector_popover.rs |  10 
crates/agent_ui/src/acp/thread_view.rs            |  92 ++++++--
crates/agent_ui/src/agent_diff.rs                 | 187 ++++++----------
crates/telemetry/src/telemetry.rs                 |   7 
16 files changed, 327 insertions(+), 188 deletions(-)

Detailed changes

Cargo.lock 🔗

@@ -32,6 +32,7 @@ dependencies = [
  "settings",
  "smol",
  "task",
+ "telemetry",
  "tempfile",
  "terminal",
  "ui",
@@ -80,6 +81,7 @@ dependencies = [
  "rand 0.9.2",
  "serde_json",
  "settings",
+ "telemetry",
  "text",
  "util",
  "watch",

crates/acp_thread/Cargo.toml 🔗

@@ -39,6 +39,7 @@ serde_json.workspace = true
 settings.workspace = true
 smol.workspace = true
 task.workspace = true
+telemetry.workspace = true
 terminal.workspace = true
 ui.workspace = true
 url.workspace = true

crates/acp_thread/src/acp_thread.rs 🔗

@@ -15,7 +15,7 @@ use settings::Settings as _;
 use task::{Shell, ShellBuilder};
 pub use terminal::*;
 
-use action_log::ActionLog;
+use action_log::{ActionLog, ActionLogTelemetry};
 use agent_client_protocol::{self as acp};
 use anyhow::{Context as _, Result, anyhow};
 use editor::Bias;
@@ -820,6 +820,15 @@ pub struct AcpThread {
     pending_terminal_exit: HashMap<acp::TerminalId, acp::TerminalExitStatus>,
 }
 
+impl From<&AcpThread> for ActionLogTelemetry {
+    fn from(value: &AcpThread) -> Self {
+        Self {
+            agent_telemetry_id: value.connection().telemetry_id(),
+            session_id: value.session_id.0.clone(),
+        }
+    }
+}
+
 #[derive(Debug)]
 pub enum AcpThreadEvent {
     NewEntry,
@@ -1346,6 +1355,17 @@ impl AcpThread {
         let path_style = self.project.read(cx).path_style(cx);
         let id = update.id.clone();
 
+        let agent = self.connection().telemetry_id();
+        let session = self.session_id();
+        if let ToolCallStatus::Completed | ToolCallStatus::Failed = status {
+            let status = if matches!(status, ToolCallStatus::Completed) {
+                "completed"
+            } else {
+                "failed"
+            };
+            telemetry::event!("Agent Tool Call Completed", agent, session, status);
+        }
+
         if let Some(ix) = self.index_for_tool_call(&id) {
             let AgentThreadEntry::ToolCall(call) = &mut self.entries[ix] else {
                 unreachable!()
@@ -1869,6 +1889,7 @@ impl AcpThread {
             return Task::ready(Err(anyhow!("not supported")));
         };
 
+        let telemetry = ActionLogTelemetry::from(&*self);
         cx.spawn(async move |this, cx| {
             cx.update(|cx| truncate.run(id.clone(), cx))?.await?;
             this.update(cx, |this, cx| {
@@ -1877,8 +1898,9 @@ impl AcpThread {
                     this.entries.truncate(ix);
                     cx.emit(AcpThreadEvent::EntriesRemoved(range));
                 }
-                this.action_log()
-                    .update(cx, |action_log, cx| action_log.reject_all_edits(cx))
+                this.action_log().update(cx, |action_log, cx| {
+                    action_log.reject_all_edits(Some(telemetry), cx)
+                })
             })?
             .await;
             Ok(())
@@ -3614,6 +3636,10 @@ mod tests {
     }
 
     impl AgentConnection for FakeAgentConnection {
+        fn telemetry_id(&self) -> &'static str {
+            "fake"
+        }
+
         fn auth_methods(&self) -> &[acp::AuthMethod] {
             &self.auth_methods
         }

crates/acp_thread/src/connection.rs 🔗

@@ -20,6 +20,8 @@ impl UserMessageId {
 }
 
 pub trait AgentConnection {
+    fn telemetry_id(&self) -> &'static str;
+
     fn new_thread(
         self: Rc<Self>,
         project: Entity<Project>,
@@ -106,9 +108,6 @@ pub trait AgentSessionSetTitle {
 }
 
 pub trait AgentTelemetry {
-    /// The name of the agent used for telemetry.
-    fn agent_name(&self) -> String;
-
     /// A representation of the current thread state that can be serialized for
     /// storage with telemetry events.
     fn thread_data(
@@ -318,6 +317,10 @@ mod test_support {
     }
 
     impl AgentConnection for StubAgentConnection {
+        fn telemetry_id(&self) -> &'static str {
+            "stub"
+        }
+
         fn auth_methods(&self) -> &[acp::AuthMethod] {
             &[]
         }

crates/action_log/Cargo.toml 🔗

@@ -20,6 +20,7 @@ futures.workspace = true
 gpui.workspace = true
 language.workspace = true
 project.workspace = true
+telemetry.workspace = true
 text.workspace = true
 util.workspace = true
 watch.workspace = true

crates/action_log/src/action_log.rs 🔗

@@ -3,7 +3,9 @@ use buffer_diff::BufferDiff;
 use clock;
 use collections::BTreeMap;
 use futures::{FutureExt, StreamExt, channel::mpsc};
-use gpui::{App, AppContext, AsyncApp, Context, Entity, Subscription, Task, WeakEntity};
+use gpui::{
+    App, AppContext, AsyncApp, Context, Entity, SharedString, Subscription, Task, WeakEntity,
+};
 use language::{Anchor, Buffer, BufferEvent, DiskState, Point, ToPoint};
 use project::{Project, ProjectItem, lsp_store::OpenLspBufferHandle};
 use std::{cmp, ops::Range, sync::Arc};
@@ -565,14 +567,17 @@ impl ActionLog {
         &mut self,
         buffer: Entity<Buffer>,
         buffer_range: Range<impl language::ToPoint>,
+        telemetry: Option<ActionLogTelemetry>,
         cx: &mut Context<Self>,
     ) {
         let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
             return;
         };
 
+        let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
         match tracked_buffer.status {
             TrackedBufferStatus::Deleted => {
+                metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
                 self.tracked_buffers.remove(&buffer);
                 cx.notify();
             }
@@ -581,7 +586,6 @@ impl ActionLog {
                 let buffer_range =
                     buffer_range.start.to_point(buffer)..buffer_range.end.to_point(buffer);
                 let mut delta = 0i32;
-
                 tracked_buffer.unreviewed_edits.retain_mut(|edit| {
                     edit.old.start = (edit.old.start as i32 + delta) as u32;
                     edit.old.end = (edit.old.end as i32 + delta) as u32;
@@ -613,6 +617,7 @@ impl ActionLog {
                                 .collect::<String>(),
                         );
                         delta += edit.new_len() as i32 - edit.old_len() as i32;
+                        metrics.add_edit(edit);
                         false
                     }
                 });
@@ -624,19 +629,24 @@ impl ActionLog {
                 tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
             }
         }
+        if let Some(telemetry) = telemetry {
+            telemetry_report_accepted_edits(&telemetry, metrics);
+        }
     }
 
     pub fn reject_edits_in_ranges(
         &mut self,
         buffer: Entity<Buffer>,
         buffer_ranges: Vec<Range<impl language::ToPoint>>,
+        telemetry: Option<ActionLogTelemetry>,
         cx: &mut Context<Self>,
     ) -> Task<Result<()>> {
         let Some(tracked_buffer) = self.tracked_buffers.get_mut(&buffer) else {
             return Task::ready(Ok(()));
         };
 
-        match &tracked_buffer.status {
+        let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
+        let task = match &tracked_buffer.status {
             TrackedBufferStatus::Created {
                 existing_file_content,
             } => {
@@ -686,6 +696,7 @@ impl ActionLog {
                     }
                 };
 
+                metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
                 self.tracked_buffers.remove(&buffer);
                 cx.notify();
                 task
@@ -699,6 +710,7 @@ impl ActionLog {
                     .update(cx, |project, cx| project.save_buffer(buffer.clone(), cx));
 
                 // Clear all tracked edits for this buffer and start over as if we just read it.
+                metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
                 self.tracked_buffers.remove(&buffer);
                 self.buffer_read(buffer.clone(), cx);
                 cx.notify();
@@ -738,6 +750,7 @@ impl ActionLog {
                         }
 
                         if revert {
+                            metrics.add_edit(edit);
                             let old_range = tracked_buffer
                                 .diff_base
                                 .point_to_offset(Point::new(edit.old.start, 0))
@@ -758,12 +771,25 @@ impl ActionLog {
                 self.project
                     .update(cx, |project, cx| project.save_buffer(buffer, cx))
             }
+        };
+        if let Some(telemetry) = telemetry {
+            telemetry_report_rejected_edits(&telemetry, metrics);
         }
+        task
     }
 
-    pub fn keep_all_edits(&mut self, cx: &mut Context<Self>) {
-        self.tracked_buffers
-            .retain(|_buffer, tracked_buffer| match tracked_buffer.status {
+    pub fn keep_all_edits(
+        &mut self,
+        telemetry: Option<ActionLogTelemetry>,
+        cx: &mut Context<Self>,
+    ) {
+        self.tracked_buffers.retain(|buffer, tracked_buffer| {
+            let mut metrics = ActionLogMetrics::for_buffer(buffer.read(cx));
+            metrics.add_edits(tracked_buffer.unreviewed_edits.edits());
+            if let Some(telemetry) = telemetry.as_ref() {
+                telemetry_report_accepted_edits(telemetry, metrics);
+            }
+            match tracked_buffer.status {
                 TrackedBufferStatus::Deleted => false,
                 _ => {
                     if let TrackedBufferStatus::Created { .. } = &mut tracked_buffer.status {
@@ -774,13 +800,24 @@ impl ActionLog {
                     tracked_buffer.schedule_diff_update(ChangeAuthor::User, cx);
                     true
                 }
-            });
+            }
+        });
+
         cx.notify();
     }
 
-    pub fn reject_all_edits(&mut self, cx: &mut Context<Self>) -> Task<()> {
+    pub fn reject_all_edits(
+        &mut self,
+        telemetry: Option<ActionLogTelemetry>,
+        cx: &mut Context<Self>,
+    ) -> Task<()> {
         let futures = self.changed_buffers(cx).into_keys().map(|buffer| {
-            let reject = self.reject_edits_in_ranges(buffer, vec![Anchor::MIN..Anchor::MAX], cx);
+            let reject = self.reject_edits_in_ranges(
+                buffer,
+                vec![Anchor::MIN..Anchor::MAX],
+                telemetry.clone(),
+                cx,
+            );
 
             async move {
                 reject.await.log_err();
@@ -788,8 +825,7 @@ impl ActionLog {
         });
 
         let task = futures::future::join_all(futures);
-
-        cx.spawn(async move |_, _| {
+        cx.background_spawn(async move {
             task.await;
         })
     }
@@ -819,6 +855,61 @@ impl ActionLog {
     }
 }
 
+#[derive(Clone)]
+pub struct ActionLogTelemetry {
+    pub agent_telemetry_id: &'static str,
+    pub session_id: Arc<str>,
+}
+
+struct ActionLogMetrics {
+    lines_removed: u32,
+    lines_added: u32,
+    language: Option<SharedString>,
+}
+
+impl ActionLogMetrics {
+    fn for_buffer(buffer: &Buffer) -> Self {
+        Self {
+            language: buffer.language().map(|l| l.name().0),
+            lines_removed: 0,
+            lines_added: 0,
+        }
+    }
+
+    fn add_edits(&mut self, edits: &[Edit<u32>]) {
+        for edit in edits {
+            self.add_edit(edit);
+        }
+    }
+
+    fn add_edit(&mut self, edit: &Edit<u32>) {
+        self.lines_added += edit.new_len();
+        self.lines_removed += edit.old_len();
+    }
+}
+
+fn telemetry_report_accepted_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
+    telemetry::event!(
+        "Agent Edits Accepted",
+        agent = telemetry.agent_telemetry_id,
+        session = telemetry.session_id,
+        language = metrics.language,
+        lines_added = metrics.lines_added,
+        lines_removed = metrics.lines_removed
+    );
+}
+
+fn telemetry_report_rejected_edits(telemetry: &ActionLogTelemetry, metrics: ActionLogMetrics) {
+    telemetry::event!(
+        "Agent Edits Rejected",
+        agent = telemetry.agent_telemetry_id,
+        session = telemetry.session_id,
+        language = metrics.language,
+        lines_added = metrics.lines_added,
+        lines_removed = metrics.lines_removed
+    );
+}
+
 fn apply_non_conflicting_edits(
     patch: &Patch<u32>,
     edits: Vec<Edit<u32>>,
@@ -1066,7 +1157,7 @@ mod tests {
         );
 
         action_log.update(cx, |log, cx| {
-            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), cx)
+            log.keep_edits_in_range(buffer.clone(), Point::new(3, 0)..Point::new(4, 3), None, cx)
         });
         cx.run_until_parked();
         assert_eq!(
@@ -1082,7 +1173,7 @@ mod tests {
         );
 
         action_log.update(cx, |log, cx| {
-            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), cx)
+            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(4, 3), None, cx)
         });
         cx.run_until_parked();
         assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1167,7 +1258,7 @@ mod tests {
         );
 
         action_log.update(cx, |log, cx| {
-            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), cx)
+            log.keep_edits_in_range(buffer.clone(), Point::new(1, 0)..Point::new(1, 0), None, cx)
         });
         cx.run_until_parked();
         assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1264,7 +1355,7 @@ mod tests {
         );
 
         action_log.update(cx, |log, cx| {
-            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
+            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), None, cx)
         });
         cx.run_until_parked();
         assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1368,7 +1459,7 @@ mod tests {
         );
 
         action_log.update(cx, |log, cx| {
-            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), cx)
+            log.keep_edits_in_range(buffer.clone(), Point::new(0, 0)..Point::new(1, 0), None, cx)
         });
         cx.run_until_parked();
         assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1427,7 +1518,7 @@ mod tests {
         );
 
         action_log.update(cx, |log, cx| {
-            log.keep_edits_in_range(buffer.clone(), 0..5, cx)
+            log.keep_edits_in_range(buffer.clone(), 0..5, None, cx)
         });
         cx.run_until_parked();
         assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -1479,7 +1570,7 @@ mod tests {
 
         action_log
             .update(cx, |log, cx| {
-                log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
+                log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx)
             })
             .await
             .unwrap();
@@ -1559,7 +1650,7 @@ mod tests {
 
         action_log
             .update(cx, |log, cx| {
-                log.reject_edits_in_ranges(buffer.clone(), vec![2..5], cx)
+                log.reject_edits_in_ranges(buffer.clone(), vec![2..5], None, cx)
             })
             .await
             .unwrap();
@@ -1742,6 +1833,7 @@ mod tests {
                 log.reject_edits_in_ranges(
                     buffer.clone(),
                     vec![Point::new(4, 0)..Point::new(4, 0)],
+                    None,
                     cx,
                 )
             })
@@ -1776,6 +1868,7 @@ mod tests {
                 log.reject_edits_in_ranges(
                     buffer.clone(),
                     vec![Point::new(0, 0)..Point::new(1, 0)],
+                    None,
                     cx,
                 )
             })
@@ -1803,6 +1896,7 @@ mod tests {
                 log.reject_edits_in_ranges(
                     buffer.clone(),
                     vec![Point::new(4, 0)..Point::new(4, 0)],
+                    None,
                     cx,
                 )
             })
@@ -1877,7 +1971,7 @@ mod tests {
             let range_2 = buffer.read(cx).anchor_before(Point::new(5, 0))
                 ..buffer.read(cx).anchor_before(Point::new(5, 3));
 
-            log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], cx)
+            log.reject_edits_in_ranges(buffer.clone(), vec![range_1, range_2], None, cx)
                 .detach();
             assert_eq!(
                 buffer.read_with(cx, |buffer, _| buffer.text()),
@@ -1938,6 +2032,7 @@ mod tests {
                 log.reject_edits_in_ranges(
                     buffer.clone(),
                     vec![Point::new(0, 0)..Point::new(0, 0)],
+                    None,
                     cx,
                 )
             })
@@ -1993,6 +2088,7 @@ mod tests {
                 log.reject_edits_in_ranges(
                     buffer.clone(),
                     vec![Point::new(0, 0)..Point::new(0, 11)],
+                    None,
                     cx,
                 )
             })
@@ -2055,6 +2151,7 @@ mod tests {
                 log.reject_edits_in_ranges(
                     buffer.clone(),
                     vec![Point::new(0, 0)..Point::new(100, 0)],
+                    None,
                     cx,
                 )
             })
@@ -2102,7 +2199,7 @@ mod tests {
 
         // User accepts the single hunk
         action_log.update(cx, |log, cx| {
-            log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, cx)
+            log.keep_edits_in_range(buffer.clone(), Anchor::MIN..Anchor::MAX, None, cx)
         });
         cx.run_until_parked();
         assert_eq!(unreviewed_hunks(&action_log, cx), vec![]);
@@ -2123,7 +2220,7 @@ mod tests {
         // User rejects the hunk
         action_log
             .update(cx, |log, cx| {
-                log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], cx)
+                log.reject_edits_in_ranges(buffer.clone(), vec![Anchor::MIN..Anchor::MAX], None, cx)
             })
             .await
             .unwrap();
@@ -2167,7 +2264,7 @@ mod tests {
         cx.run_until_parked();
 
         // User clicks "Accept All"
-        action_log.update(cx, |log, cx| log.keep_all_edits(cx));
+        action_log.update(cx, |log, cx| log.keep_all_edits(None, cx));
         cx.run_until_parked();
         assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
         assert_eq!(unreviewed_hunks(&action_log, cx), vec![]); // Hunks are cleared
@@ -2186,7 +2283,7 @@ mod tests {
 
         // User clicks "Reject All"
         action_log
-            .update(cx, |log, cx| log.reject_all_edits(cx))
+            .update(cx, |log, cx| log.reject_all_edits(None, cx))
             .await;
         cx.run_until_parked();
         assert!(fs.is_file(path!("/dir/new_file").as_ref()).await);
@@ -2226,7 +2323,7 @@ mod tests {
                     action_log.update(cx, |log, cx| {
                         let range = buffer.read(cx).random_byte_range(0, &mut rng);
                         log::info!("keeping edits in range {:?}", range);
-                        log.keep_edits_in_range(buffer.clone(), range, cx)
+                        log.keep_edits_in_range(buffer.clone(), range, None, cx)
                     });
                 }
                 25..50 => {
@@ -2234,7 +2331,7 @@ mod tests {
                         .update(cx, |log, cx| {
                             let range = buffer.read(cx).random_byte_range(0, &mut rng);
                             log::info!("rejecting edits in range {:?}", range);
-                            log.reject_edits_in_ranges(buffer.clone(), vec![range], cx)
+                            log.reject_edits_in_ranges(buffer.clone(), vec![range], None, cx)
                         })
                         .await
                         .unwrap();

crates/agent/src/agent.rs 🔗

@@ -967,6 +967,10 @@ impl acp_thread::AgentModelSelector for NativeAgentModelSelector {
 }
 
 impl acp_thread::AgentConnection for NativeAgentConnection {
+    fn telemetry_id(&self) -> &'static str {
+        "zed"
+    }
+
     fn new_thread(
         self: Rc<Self>,
         project: Entity<Project>,
@@ -1107,10 +1111,6 @@ impl acp_thread::AgentConnection for NativeAgentConnection {
 }
 
 impl acp_thread::AgentTelemetry for NativeAgentConnection {
-    fn agent_name(&self) -> String {
-        "Zed".into()
-    }
-
     fn thread_data(
         &self,
         session_id: &acp::SessionId,

crates/agent_servers/src/acp.rs 🔗

@@ -29,6 +29,7 @@ pub struct UnsupportedVersion;
 
 pub struct AcpConnection {
     server_name: SharedString,
+    telemetry_id: &'static str,
     connection: Rc<acp::ClientSideConnection>,
     sessions: Rc<RefCell<HashMap<acp::SessionId, AcpSession>>>,
     auth_methods: Vec<acp::AuthMethod>,
@@ -52,6 +53,7 @@ pub struct AcpSession {
 
 pub async fn connect(
     server_name: SharedString,
+    telemetry_id: &'static str,
     command: AgentServerCommand,
     root_dir: &Path,
     default_mode: Option<acp::SessionModeId>,
@@ -60,6 +62,7 @@ pub async fn connect(
 ) -> Result<Rc<dyn AgentConnection>> {
     let conn = AcpConnection::stdio(
         server_name,
+        telemetry_id,
         command.clone(),
         root_dir,
         default_mode,
@@ -75,6 +78,7 @@ const MINIMUM_SUPPORTED_VERSION: acp::ProtocolVersion = acp::V1;
 impl AcpConnection {
     pub async fn stdio(
         server_name: SharedString,
+        telemetry_id: &'static str,
         command: AgentServerCommand,
         root_dir: &Path,
         default_mode: Option<acp::SessionModeId>,
@@ -199,6 +203,7 @@ impl AcpConnection {
             root_dir: root_dir.to_owned(),
             connection,
             server_name,
+            telemetry_id,
             sessions,
             agent_capabilities: response.agent_capabilities,
             default_mode,
@@ -226,6 +231,10 @@ impl Drop for AcpConnection {
 }
 
 impl AgentConnection for AcpConnection {
+    fn telemetry_id(&self) -> &'static str {
+        self.telemetry_id
+    }
+
     fn new_thread(
         self: Rc<Self>,
         project: Entity<Project>,

crates/agent_servers/src/claude.rs 🔗

@@ -62,6 +62,7 @@ impl AgentServer for ClaudeCode {
         cx: &mut App,
     ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
         let name = self.name();
+        let telemetry_id = self.telemetry_id();
         let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
         let is_remote = delegate.project.read(cx).is_via_remote_server();
         let store = delegate.store.downgrade();
@@ -85,6 +86,7 @@ impl AgentServer for ClaudeCode {
                 .await?;
             let connection = crate::acp::connect(
                 name,
+                telemetry_id,
                 command,
                 root_dir.as_ref(),
                 default_mode,

crates/agent_servers/src/codex.rs 🔗

@@ -63,6 +63,7 @@ impl AgentServer for Codex {
         cx: &mut App,
     ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
         let name = self.name();
+        let telemetry_id = self.telemetry_id();
         let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
         let is_remote = delegate.project.read(cx).is_via_remote_server();
         let store = delegate.store.downgrade();
@@ -87,6 +88,7 @@ impl AgentServer for Codex {
 
             let connection = crate::acp::connect(
                 name,
+                telemetry_id,
                 command,
                 root_dir.as_ref(),
                 default_mode,

crates/agent_servers/src/custom.rs 🔗

@@ -67,6 +67,7 @@ impl crate::AgentServer for CustomAgentServer {
         cx: &mut App,
     ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
         let name = self.name();
+        let telemetry_id = self.telemetry_id();
         let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
         let is_remote = delegate.project.read(cx).is_via_remote_server();
         let default_mode = self.default_mode(cx);
@@ -92,6 +93,7 @@ impl crate::AgentServer for CustomAgentServer {
                 .await?;
             let connection = crate::acp::connect(
                 name,
+                telemetry_id,
                 command,
                 root_dir.as_ref(),
                 default_mode,

crates/agent_servers/src/gemini.rs 🔗

@@ -31,6 +31,7 @@ impl AgentServer for Gemini {
         cx: &mut App,
     ) -> Task<Result<(Rc<dyn AgentConnection>, Option<task::SpawnInTerminal>)>> {
         let name = self.name();
+        let telemetry_id = self.telemetry_id();
         let root_dir = root_dir.map(|root_dir| root_dir.to_string_lossy().into_owned());
         let is_remote = delegate.project.read(cx).is_via_remote_server();
         let store = delegate.store.downgrade();
@@ -64,6 +65,7 @@ impl AgentServer for Gemini {
 
             let connection = crate::acp::connect(
                 name,
+                telemetry_id,
                 command,
                 root_dir.as_ref(),
                 default_mode,

crates/agent_ui/src/acp/model_selector_popover.rs 🔗

@@ -1,6 +1,6 @@
 use std::rc::Rc;
 
-use acp_thread::AgentModelSelector;
+use acp_thread::{AgentModelInfo, AgentModelSelector};
 use gpui::{Entity, FocusHandle};
 use picker::popover_menu::PickerPopoverMenu;
 use ui::{
@@ -36,12 +36,8 @@ impl AcpModelSelectorPopover {
         self.menu_handle.toggle(window, cx);
     }
 
-    pub fn active_model_name(&self, cx: &App) -> Option<SharedString> {
-        self.selector
-            .read(cx)
-            .delegate
-            .active_model()
-            .map(|model| model.name.clone())
+    pub fn active_model<'a>(&self, cx: &'a App) -> Option<&'a AgentModelInfo> {
+        self.selector.read(cx).delegate.active_model()
     }
 }
 

crates/agent_ui/src/acp/thread_view.rs 🔗

@@ -4,7 +4,7 @@ use acp_thread::{
     ToolCallStatus, UserMessageId,
 };
 use acp_thread::{AgentConnection, Plan};
-use action_log::ActionLog;
+use action_log::{ActionLog, ActionLogTelemetry};
 use agent::{DbThreadMetadata, HistoryEntry, HistoryEntryId, HistoryStore, NativeAgentServer};
 use agent_client_protocol::{self as acp, PromptCapabilities};
 use agent_servers::{AgentServer, AgentServerDelegate};
@@ -169,7 +169,7 @@ impl ThreadFeedbackState {
             }
         }
         let session_id = thread.read(cx).session_id().clone();
-        let agent_name = telemetry.agent_name();
+        let agent = thread.read(cx).connection().telemetry_id();
         let task = telemetry.thread_data(&session_id, cx);
         let rating = match feedback {
             ThreadFeedback::Positive => "positive",
@@ -179,9 +179,9 @@ impl ThreadFeedbackState {
             let thread = task.await?;
             telemetry::event!(
                 "Agent Thread Rated",
+                agent = agent,
                 session_id = session_id,
                 rating = rating,
-                agent = agent_name,
                 thread = thread
             );
             anyhow::Ok(())
@@ -206,15 +206,15 @@ impl ThreadFeedbackState {
         self.comments_editor.take();
 
         let session_id = thread.read(cx).session_id().clone();
-        let agent_name = telemetry.agent_name();
+        let agent = thread.read(cx).connection().telemetry_id();
         let task = telemetry.thread_data(&session_id, cx);
         cx.background_spawn(async move {
             let thread = task.await?;
             telemetry::event!(
                 "Agent Thread Feedback Comments",
+                agent = agent,
                 session_id = session_id,
                 comments = comments,
-                agent = agent_name,
                 thread = thread
             );
             anyhow::Ok(())
@@ -1123,8 +1123,6 @@ impl AcpThreadView {
             message_editor.contents(full_mention_content, cx)
         });
 
-        let agent_telemetry_id = self.agent.telemetry_id();
-
         self.thread_error.take();
         self.editing_message.take();
         self.thread_feedback.clear();
@@ -1132,6 +1130,8 @@ impl AcpThreadView {
         let Some(thread) = self.thread() else {
             return;
         };
+        let agent_telemetry_id = self.agent.telemetry_id();
+        let session_id = thread.read(cx).session_id().clone();
         let thread = thread.downgrade();
         if self.should_be_following {
             self.workspace
@@ -1142,6 +1142,7 @@ impl AcpThreadView {
         }
 
         self.is_loading_contents = true;
+        let model_id = self.current_model_id(cx);
         let guard = cx.new(|_| ());
         cx.observe_release(&guard, |this, _guard, cx| {
             this.is_loading_contents = false;
@@ -1163,6 +1164,7 @@ impl AcpThreadView {
                     message_editor.clear(window, cx);
                 });
             })?;
+            let turn_start_time = Instant::now();
             let send = thread.update(cx, |thread, cx| {
                 thread.action_log().update(cx, |action_log, cx| {
                     for buffer in tracked_buffers {
@@ -1171,11 +1173,27 @@ impl AcpThreadView {
                 });
                 drop(guard);
 
-                telemetry::event!("Agent Message Sent", agent = agent_telemetry_id);
+                telemetry::event!(
+                    "Agent Message Sent",
+                    agent = agent_telemetry_id,
+                    session = session_id,
+                    model = model_id
+                );
 
                 thread.send(contents, cx)
             })?;
-            send.await
+            let res = send.await;
+            let turn_time_ms = turn_start_time.elapsed().as_millis();
+            let status = if res.is_ok() { "success" } else { "failure" };
+            telemetry::event!(
+                "Agent Turn Completed",
+                agent = agent_telemetry_id,
+                session = session_id,
+                model = model_id,
+                status,
+                turn_time_ms,
+            );
+            res
         });
 
         cx.spawn(async move |this, cx| {
@@ -1377,7 +1395,7 @@ impl AcpThreadView {
             AcpThreadEvent::Refusal => {
                 self.thread_retry_status.take();
                 self.thread_error = Some(ThreadError::Refusal);
-                let model_or_agent_name = self.get_current_model_name(cx);
+                let model_or_agent_name = self.current_model_name(cx);
                 let notification_message =
                     format!("{} refused to respond to this request", model_or_agent_name);
                 self.notify_with_sound(&notification_message, IconName::Warning, window, cx);
@@ -1846,6 +1864,14 @@ impl AcpThreadView {
         let Some(thread) = self.thread() else {
             return;
         };
+
+        telemetry::event!(
+            "Agent Tool Call Authorized",
+            agent = self.agent.telemetry_id(),
+            session = thread.read(cx).session_id(),
+            option = option_kind
+        );
+
         thread.update(cx, |thread, cx| {
             thread.authorize_tool_call(tool_call_id, option_id, option_kind, cx);
         });
@@ -3578,6 +3604,7 @@ impl AcpThreadView {
     ) -> Option<AnyElement> {
         let thread = thread_entity.read(cx);
         let action_log = thread.action_log();
+        let telemetry = ActionLogTelemetry::from(thread);
         let changed_buffers = action_log.read(cx).changed_buffers(cx);
         let plan = thread.plan();
 
@@ -3625,6 +3652,7 @@ impl AcpThreadView {
                 .when(self.edits_expanded, |parent| {
                     parent.child(self.render_edited_files(
                         action_log,
+                        telemetry,
                         &changed_buffers,
                         pending_edits,
                         cx,
@@ -3905,6 +3933,7 @@ impl AcpThreadView {
     fn render_edited_files(
         &self,
         action_log: &Entity<ActionLog>,
+        telemetry: ActionLogTelemetry,
         changed_buffers: &BTreeMap<Entity<Buffer>, Entity<BufferDiff>>,
         pending_edits: bool,
         cx: &Context<Self>,
@@ -4024,12 +4053,14 @@ impl AcpThreadView {
                                     .on_click({
                                         let buffer = buffer.clone();
                                         let action_log = action_log.clone();
+                                        let telemetry = telemetry.clone();
                                         move |_, _, cx| {
                                             action_log.update(cx, |action_log, cx| {
                                                 action_log
                                                     .reject_edits_in_ranges(
                                                         buffer.clone(),
                                                         vec![Anchor::MIN..Anchor::MAX],
+                                                        Some(telemetry.clone()),
                                                         cx,
                                                     )
                                                     .detach_and_log_err(cx);
@@ -4044,11 +4075,13 @@ impl AcpThreadView {
                                     .on_click({
                                         let buffer = buffer.clone();
                                         let action_log = action_log.clone();
+                                        let telemetry = telemetry.clone();
                                         move |_, _, cx| {
                                             action_log.update(cx, |action_log, cx| {
                                                 action_log.keep_edits_in_range(
                                                     buffer.clone(),
                                                     Anchor::MIN..Anchor::MAX,
+                                                    Some(telemetry.clone()),
                                                     cx,
                                                 );
                                             })
@@ -4264,17 +4297,23 @@ impl AcpThreadView {
         let Some(thread) = self.thread() else {
             return;
         };
+        let telemetry = ActionLogTelemetry::from(thread.read(cx));
         let action_log = thread.read(cx).action_log().clone();
-        action_log.update(cx, |action_log, cx| action_log.keep_all_edits(cx));
+        action_log.update(cx, |action_log, cx| {
+            action_log.keep_all_edits(Some(telemetry), cx)
+        });
     }
 
     fn reject_all(&mut self, _: &RejectAll, _window: &mut Window, cx: &mut Context<Self>) {
         let Some(thread) = self.thread() else {
             return;
         };
+        let telemetry = ActionLogTelemetry::from(thread.read(cx));
         let action_log = thread.read(cx).action_log().clone();
         action_log
-            .update(cx, |action_log, cx| action_log.reject_all_edits(cx))
+            .update(cx, |action_log, cx| {
+                action_log.reject_all_edits(Some(telemetry), cx)
+            })
             .detach();
     }
 
@@ -5334,20 +5373,21 @@ impl AcpThreadView {
         )
     }
 
-    fn get_current_model_name(&self, cx: &App) -> SharedString {
+    fn current_model_id(&self, cx: &App) -> Option<String> {
+        self.model_selector
+            .as_ref()
+            .and_then(|selector| selector.read(cx).active_model(cx).map(|m| m.id.to_string()))
+    }
+
+    fn current_model_name(&self, cx: &App) -> SharedString {
         // For native agent (Zed Agent), use the specific model name (e.g., "Claude 3.5 Sonnet")
         // For ACP agents, use the agent name (e.g., "Claude Code", "Gemini CLI")
         // This provides better clarity about what refused the request
-        if self
-            .agent
-            .clone()
-            .downcast::<agent::NativeAgentServer>()
-            .is_some()
-        {
-            // Native agent - use the model name
+        if self.as_native_connection(cx).is_some() {
             self.model_selector
                 .as_ref()
-                .and_then(|selector| selector.read(cx).active_model_name(cx))
+                .and_then(|selector| selector.read(cx).active_model(cx))
+                .map(|model| model.name.clone())
                 .unwrap_or_else(|| SharedString::from("The model"))
         } else {
             // ACP agent - use the agent name (e.g., "Claude Code", "Gemini CLI")
@@ -5356,7 +5396,7 @@ impl AcpThreadView {
     }
 
     fn render_refusal_error(&self, cx: &mut Context<'_, Self>) -> Callout {
-        let model_or_agent_name = self.get_current_model_name(cx);
+        let model_or_agent_name = self.current_model_name(cx);
         let refusal_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
@@ -6342,6 +6382,10 @@ pub(crate) mod tests {
     struct SaboteurAgentConnection;
 
     impl AgentConnection for SaboteurAgentConnection {
+        fn telemetry_id(&self) -> &'static str {
+            "saboteur"
+        }
+
         fn new_thread(
             self: Rc<Self>,
             project: Entity<Project>,
@@ -6402,6 +6446,10 @@ pub(crate) mod tests {
     struct RefusalAgentConnection;
 
     impl AgentConnection for RefusalAgentConnection {
+        fn telemetry_id(&self) -> &'static str {
+            "refusal"
+        }
+
         fn new_thread(
             self: Rc<Self>,
             project: Entity<Project>,

crates/agent_ui/src/agent_diff.rs 🔗

@@ -1,6 +1,6 @@
 use crate::{Keep, KeepAll, OpenAgentDiff, Reject, RejectAll};
 use acp_thread::{AcpThread, AcpThreadEvent};
-use action_log::ActionLog;
+use action_log::ActionLogTelemetry;
 use agent_settings::AgentSettings;
 use anyhow::Result;
 use buffer_diff::DiffHunkStatus;
@@ -40,79 +40,16 @@ use zed_actions::assistant::ToggleFocus;
 pub struct AgentDiffPane {
     multibuffer: Entity<MultiBuffer>,
     editor: Entity<Editor>,
-    thread: AgentDiffThread,
+    thread: Entity<AcpThread>,
     focus_handle: FocusHandle,
     workspace: WeakEntity<Workspace>,
     title: SharedString,
     _subscriptions: Vec<Subscription>,
 }
 
-#[derive(PartialEq, Eq, Clone)]
-pub enum AgentDiffThread {
-    AcpThread(Entity<AcpThread>),
-}
-
-impl AgentDiffThread {
-    fn project(&self, cx: &App) -> Entity<Project> {
-        match self {
-            AgentDiffThread::AcpThread(thread) => thread.read(cx).project().clone(),
-        }
-    }
-    fn action_log(&self, cx: &App) -> Entity<ActionLog> {
-        match self {
-            AgentDiffThread::AcpThread(thread) => thread.read(cx).action_log().clone(),
-        }
-    }
-
-    fn title(&self, cx: &App) -> SharedString {
-        match self {
-            AgentDiffThread::AcpThread(thread) => thread.read(cx).title(),
-        }
-    }
-
-    fn has_pending_edit_tool_uses(&self, cx: &App) -> bool {
-        match self {
-            AgentDiffThread::AcpThread(thread) => thread.read(cx).has_pending_edit_tool_calls(),
-        }
-    }
-
-    fn downgrade(&self) -> WeakAgentDiffThread {
-        match self {
-            AgentDiffThread::AcpThread(thread) => {
-                WeakAgentDiffThread::AcpThread(thread.downgrade())
-            }
-        }
-    }
-}
-
-impl From<Entity<AcpThread>> for AgentDiffThread {
-    fn from(entity: Entity<AcpThread>) -> Self {
-        AgentDiffThread::AcpThread(entity)
-    }
-}
-
-#[derive(PartialEq, Eq, Clone)]
-pub enum WeakAgentDiffThread {
-    AcpThread(WeakEntity<AcpThread>),
-}
-
-impl WeakAgentDiffThread {
-    pub fn upgrade(&self) -> Option<AgentDiffThread> {
-        match self {
-            WeakAgentDiffThread::AcpThread(weak) => weak.upgrade().map(AgentDiffThread::AcpThread),
-        }
-    }
-}
-
-impl From<WeakEntity<AcpThread>> for WeakAgentDiffThread {
-    fn from(entity: WeakEntity<AcpThread>) -> Self {
-        WeakAgentDiffThread::AcpThread(entity)
-    }
-}
-
 impl AgentDiffPane {
     pub fn deploy(
-        thread: impl Into<AgentDiffThread>,
+        thread: Entity<AcpThread>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut App,
@@ -123,12 +60,11 @@ impl AgentDiffPane {
     }
 
     pub fn deploy_in_workspace(
-        thread: impl Into<AgentDiffThread>,
+        thread: Entity<AcpThread>,
         workspace: &mut Workspace,
         window: &mut Window,
         cx: &mut Context<Workspace>,
     ) -> Entity<Self> {
-        let thread = thread.into();
         let existing_diff = workspace
             .items_of_type::<AgentDiffPane>(cx)
             .find(|diff| diff.read(cx).thread == thread);
@@ -145,7 +81,7 @@ impl AgentDiffPane {
     }
 
     pub fn new(
-        thread: AgentDiffThread,
+        thread: Entity<AcpThread>,
         workspace: WeakEntity<Workspace>,
         window: &mut Window,
         cx: &mut Context<Self>,
@@ -153,7 +89,7 @@ impl AgentDiffPane {
         let focus_handle = cx.focus_handle();
         let multibuffer = cx.new(|_| MultiBuffer::new(Capability::ReadWrite));
 
-        let project = thread.project(cx);
+        let project = thread.read(cx).project().clone();
         let editor = cx.new(|cx| {
             let mut editor =
                 Editor::for_multibuffer(multibuffer.clone(), Some(project.clone()), window, cx);
@@ -164,19 +100,16 @@ impl AgentDiffPane {
             editor
         });
 
-        let action_log = thread.action_log(cx);
+        let action_log = thread.read(cx).action_log().clone();
 
         let mut this = Self {
             _subscriptions: vec![
                 cx.observe_in(&action_log, window, |this, _action_log, window, cx| {
                     this.update_excerpts(window, cx)
                 }),
-                match &thread {
-                    AgentDiffThread::AcpThread(thread) => cx
-                        .subscribe(thread, |this, _thread, event, cx| {
-                            this.handle_acp_thread_event(event, cx)
-                        }),
-                },
+                cx.subscribe(&thread, |this, _thread, event, cx| {
+                    this.handle_acp_thread_event(event, cx)
+                }),
             ],
             title: SharedString::default(),
             multibuffer,
@@ -191,7 +124,12 @@ impl AgentDiffPane {
     }
 
     fn update_excerpts(&mut self, window: &mut Window, cx: &mut Context<Self>) {
-        let changed_buffers = self.thread.action_log(cx).read(cx).changed_buffers(cx);
+        let changed_buffers = self
+            .thread
+            .read(cx)
+            .action_log()
+            .read(cx)
+            .changed_buffers(cx);
         let mut paths_to_delete = self.multibuffer.read(cx).paths().collect::<HashSet<_>>();
 
         for (buffer, diff_handle) in changed_buffers {
@@ -278,7 +216,7 @@ impl AgentDiffPane {
     }
 
     fn update_title(&mut self, cx: &mut Context<Self>) {
-        let new_title = self.thread.title(cx);
+        let new_title = self.thread.read(cx).title();
         if new_title != self.title {
             self.title = new_title;
             cx.emit(EditorEvent::TitleChanged);
@@ -340,16 +278,18 @@ impl AgentDiffPane {
     }
 
     fn keep_all(&mut self, _: &KeepAll, _window: &mut Window, cx: &mut Context<Self>) {
-        self.thread
-            .action_log(cx)
-            .update(cx, |action_log, cx| action_log.keep_all_edits(cx))
+        let telemetry = ActionLogTelemetry::from(self.thread.read(cx));
+        let action_log = self.thread.read(cx).action_log().clone();
+        action_log.update(cx, |action_log, cx| {
+            action_log.keep_all_edits(Some(telemetry), cx)
+        });
     }
 }
 
 fn keep_edits_in_selection(
     editor: &mut Editor,
     buffer_snapshot: &MultiBufferSnapshot,
-    thread: &AgentDiffThread,
+    thread: &Entity<AcpThread>,
     window: &mut Window,
     cx: &mut Context<Editor>,
 ) {
@@ -364,7 +304,7 @@ fn keep_edits_in_selection(
 fn reject_edits_in_selection(
     editor: &mut Editor,
     buffer_snapshot: &MultiBufferSnapshot,
-    thread: &AgentDiffThread,
+    thread: &Entity<AcpThread>,
     window: &mut Window,
     cx: &mut Context<Editor>,
 ) {
@@ -378,7 +318,7 @@ fn reject_edits_in_selection(
 fn keep_edits_in_ranges(
     editor: &mut Editor,
     buffer_snapshot: &MultiBufferSnapshot,
-    thread: &AgentDiffThread,
+    thread: &Entity<AcpThread>,
     ranges: Vec<Range<editor::Anchor>>,
     window: &mut Window,
     cx: &mut Context<Editor>,
@@ -393,8 +333,15 @@ fn keep_edits_in_ranges(
     for hunk in &diff_hunks_in_ranges {
         let buffer = multibuffer.read(cx).buffer(hunk.buffer_id);
         if let Some(buffer) = buffer {
-            thread.action_log(cx).update(cx, |action_log, cx| {
-                action_log.keep_edits_in_range(buffer, hunk.buffer_range.clone(), cx)
+            let action_log = thread.read(cx).action_log().clone();
+            let telemetry = ActionLogTelemetry::from(thread.read(cx));
+            action_log.update(cx, |action_log, cx| {
+                action_log.keep_edits_in_range(
+                    buffer,
+                    hunk.buffer_range.clone(),
+                    Some(telemetry),
+                    cx,
+                )
             });
         }
     }
@@ -403,7 +350,7 @@ fn keep_edits_in_ranges(
 fn reject_edits_in_ranges(
     editor: &mut Editor,
     buffer_snapshot: &MultiBufferSnapshot,
-    thread: &AgentDiffThread,
+    thread: &Entity<AcpThread>,
     ranges: Vec<Range<editor::Anchor>>,
     window: &mut Window,
     cx: &mut Context<Editor>,
@@ -427,11 +374,12 @@ fn reject_edits_in_ranges(
         }
     }
 
+    let action_log = thread.read(cx).action_log().clone();
+    let telemetry = ActionLogTelemetry::from(thread.read(cx));
     for (buffer, ranges) in ranges_by_buffer {
-        thread
-            .action_log(cx)
+        action_log
             .update(cx, |action_log, cx| {
-                action_log.reject_edits_in_ranges(buffer, ranges, cx)
+                action_log.reject_edits_in_ranges(buffer, ranges, Some(telemetry.clone()), cx)
             })
             .detach_and_log_err(cx);
     }
@@ -531,7 +479,7 @@ impl Item for AgentDiffPane {
     }
 
     fn tab_content(&self, params: TabContentParams, _window: &Window, cx: &App) -> AnyElement {
-        let title = self.thread.title(cx);
+        let title = self.thread.read(cx).title();
         Label::new(format!("Review: {}", title))
             .color(if params.selected {
                 Color::Default
@@ -712,7 +660,7 @@ impl Render for AgentDiffPane {
     }
 }
 
-fn diff_hunk_controls(thread: &AgentDiffThread) -> editor::RenderDiffHunkControlsFn {
+fn diff_hunk_controls(thread: &Entity<AcpThread>) -> editor::RenderDiffHunkControlsFn {
     let thread = thread.clone();
 
     Arc::new(
@@ -739,7 +687,7 @@ fn render_diff_hunk_controls(
     hunk_range: Range<editor::Anchor>,
     is_created_file: bool,
     line_height: Pixels,
-    thread: &AgentDiffThread,
+    thread: &Entity<AcpThread>,
     editor: &Entity<Editor>,
     cx: &mut App,
 ) -> AnyElement {
@@ -1153,8 +1101,11 @@ impl Render for AgentDiffToolbar {
                     return Empty.into_any();
                 };
 
-                let has_pending_edit_tool_use =
-                    agent_diff.read(cx).thread.has_pending_edit_tool_uses(cx);
+                let has_pending_edit_tool_use = agent_diff
+                    .read(cx)
+                    .thread
+                    .read(cx)
+                    .has_pending_edit_tool_calls();
 
                 if has_pending_edit_tool_use {
                     return div().px_2().child(spinner_icon).into_any();
@@ -1214,7 +1165,7 @@ pub enum EditorState {
 }
 
 struct WorkspaceThread {
-    thread: WeakAgentDiffThread,
+    thread: WeakEntity<AcpThread>,
     _thread_subscriptions: (Subscription, Subscription),
     singleton_editors: HashMap<WeakEntity<Buffer>, HashMap<WeakEntity<Editor>, Subscription>>,
     _settings_subscription: Subscription,
@@ -1239,23 +1190,23 @@ impl AgentDiff {
 
     pub fn set_active_thread(
         workspace: &WeakEntity<Workspace>,
-        thread: impl Into<AgentDiffThread>,
+        thread: Entity<AcpThread>,
         window: &mut Window,
         cx: &mut App,
     ) {
         Self::global(cx).update(cx, |this, cx| {
-            this.register_active_thread_impl(workspace, thread.into(), window, cx);
+            this.register_active_thread_impl(workspace, thread, window, cx);
         });
     }
 
     fn register_active_thread_impl(
         &mut self,
         workspace: &WeakEntity<Workspace>,
-        thread: AgentDiffThread,
+        thread: Entity<AcpThread>,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) {
-        let action_log = thread.action_log(cx);
+        let action_log = thread.read(cx).action_log().clone();
 
         let action_log_subscription = cx.observe_in(&action_log, window, {
             let workspace = workspace.clone();
@@ -1264,14 +1215,12 @@ impl AgentDiff {
             }
         });
 
-        let thread_subscription = match &thread {
-            AgentDiffThread::AcpThread(thread) => cx.subscribe_in(thread, window, {
-                let workspace = workspace.clone();
-                move |this, thread, event, window, cx| {
-                    this.handle_acp_thread_event(&workspace, thread, event, window, cx)
-                }
-            }),
-        };
+        let thread_subscription = cx.subscribe_in(&thread, window, {
+            let workspace = workspace.clone();
+            move |this, thread, event, window, cx| {
+                this.handle_acp_thread_event(&workspace, thread, event, window, cx)
+            }
+        });
 
         if let Some(workspace_thread) = self.workspace_threads.get_mut(workspace) {
             // replace thread and action log subscription, but keep editors
@@ -1348,7 +1297,7 @@ impl AgentDiff {
 
     fn register_review_action<T: Action>(
         workspace: &mut Workspace,
-        review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState
+        review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState
         + 'static,
         this: &Entity<AgentDiff>,
     ) {
@@ -1508,7 +1457,7 @@ impl AgentDiff {
             return;
         };
 
-        let action_log = thread.action_log(cx);
+        let action_log = thread.read(cx).action_log();
         let changed_buffers = action_log.read(cx).changed_buffers(cx);
 
         let mut unaffected = self.reviewing_editors.clone();
@@ -1627,7 +1576,7 @@ impl AgentDiff {
 
     fn keep_all(
         editor: &Entity<Editor>,
-        thread: &AgentDiffThread,
+        thread: &Entity<AcpThread>,
         window: &mut Window,
         cx: &mut App,
     ) -> PostReviewState {
@@ -1647,7 +1596,7 @@ impl AgentDiff {
 
     fn reject_all(
         editor: &Entity<Editor>,
-        thread: &AgentDiffThread,
+        thread: &Entity<AcpThread>,
         window: &mut Window,
         cx: &mut App,
     ) -> PostReviewState {
@@ -1667,7 +1616,7 @@ impl AgentDiff {
 
     fn keep(
         editor: &Entity<Editor>,
-        thread: &AgentDiffThread,
+        thread: &Entity<AcpThread>,
         window: &mut Window,
         cx: &mut App,
     ) -> PostReviewState {
@@ -1680,7 +1629,7 @@ impl AgentDiff {
 
     fn reject(
         editor: &Entity<Editor>,
-        thread: &AgentDiffThread,
+        thread: &Entity<AcpThread>,
         window: &mut Window,
         cx: &mut App,
     ) -> PostReviewState {
@@ -1703,7 +1652,7 @@ impl AgentDiff {
     fn review_in_active_editor(
         &mut self,
         workspace: &mut Workspace,
-        review: impl Fn(&Entity<Editor>, &AgentDiffThread, &mut Window, &mut App) -> PostReviewState,
+        review: impl Fn(&Entity<Editor>, &Entity<AcpThread>, &mut Window, &mut App) -> PostReviewState,
         window: &mut Window,
         cx: &mut Context<Self>,
     ) -> Option<Task<Result<()>>> {
@@ -1725,7 +1674,7 @@ impl AgentDiff {
         if let PostReviewState::AllReviewed = review(&editor, &thread, window, cx)
             && let Some(curr_buffer) = editor.read(cx).buffer().read(cx).as_singleton()
         {
-            let changed_buffers = thread.action_log(cx).read(cx).changed_buffers(cx);
+            let changed_buffers = thread.read(cx).action_log().read(cx).changed_buffers(cx);
 
             let mut keys = changed_buffers.keys().cycle();
             keys.find(|k| *k == &curr_buffer);
@@ -1815,8 +1764,7 @@ mod tests {
             .await
             .unwrap();
 
-        let thread = AgentDiffThread::AcpThread(thread);
-        let action_log = cx.read(|cx| thread.action_log(cx));
+        let action_log = cx.read(|cx| thread.read(cx).action_log().clone());
 
         let (workspace, cx) =
             cx.add_window_view(|window, cx| Workspace::test_new(project.clone(), window, cx));
@@ -2004,7 +1952,6 @@ mod tests {
         let action_log = thread.read_with(cx, |thread, _| thread.action_log().clone());
 
         // Set the active thread
-        let thread = AgentDiffThread::AcpThread(thread);
         cx.update(|window, cx| {
             AgentDiff::set_active_thread(&workspace.downgrade(), thread.clone(), window, cx)
         });

crates/telemetry/src/telemetry.rs 🔗

@@ -54,9 +54,10 @@ macro_rules! serialize_property {
 }
 
 pub fn send_event(event: Event) {
-    if let Some(queue) = TELEMETRY_QUEUE.get() {
-        queue.unbounded_send(event).ok();
-    }
+    println!("{} - {:?}", event.event_type, event.event_properties);
+    // if let Some(queue) = TELEMETRY_QUEUE.get() {
+    // queue.unbounded_send(event).ok();
+    // }
 }
 
 pub fn init(tx: mpsc::UnboundedSender<Event>) {