From 7eecad8fde8a7a5ff575660114eb02f3f79f8a62 Mon Sep 17 00:00:00 2001 From: "zed-zippy[bot]" <234243425+zed-zippy[bot]@users.noreply.github.com> Date: Wed, 4 Feb 2026 21:31:52 +0000 Subject: [PATCH] edit_prediction: Add Mercury accept/reject tracking (#48306) (cherry-pick to preview) (#48402) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of #48306 to preview ---- ### Summary Adds accept/reject tracking for Mercury edit predictions. ### Changes Sends events to https://api-feedback.inceptionlabs.ai/feedback when: Accept — user presses Tab Reject — user presses Escape Ignore — prediction dismissed implicitly (typing, cursor move, etc.) Added `discard_explicit` method to the delegate trait to distinguish explicit vs implicit dismissal. Updated `reject_prediction` and `reject_current_prediction` methods with an `explicit` bool parameter to thread this through to the Mercury feedback logic. Other providers are unaffected—they use the default implementation. Feedback is fire-and-forget in a background thread, only sent for predictions that were shown. ### Data Collected - Request ID (returned from Inception API) - User action (either accept/reject/ignore) - Client Zed version (to track updates made to Zed client which could potentially affect nextedit implementation) Release Notes: - N/A --------- Co-authored-by: Ben Kunkle Co-authored-by: Kenan Hasanaliyev Co-authored-by: Ben Kunkle --- crates/codestral/src/codestral.rs | 6 +- .../src/copilot_edit_prediction_delegate.rs | 5 +- crates/edit_prediction/src/edit_prediction.rs | 68 +++++++++---- .../src/edit_prediction_tests.rs | 27 ++++-- crates/edit_prediction/src/mercury.rs | 95 ++++++++++++++++++- .../src/zed_edit_prediction_delegate.rs | 16 +++- .../src/edit_prediction_types.rs | 14 ++- crates/editor/src/edit_prediction_tests.rs | 14 ++- crates/editor/src/editor.rs | 11 ++- .../supermaven_edit_prediction_delegate.rs | 6 +- 10 files changed, 219 insertions(+), 43 deletions(-) diff --git a/crates/codestral/src/codestral.rs b/crates/codestral/src/codestral.rs index 327a8b06c1b844023adb577874833c3dfd5bc91d..0720673ff784b68145cc28f8245ee5147db5779a 100644 --- a/crates/codestral/src/codestral.rs +++ b/crates/codestral/src/codestral.rs @@ -1,6 +1,8 @@ use anyhow::Result; use edit_prediction::cursor_excerpt; -use edit_prediction_types::{EditPrediction, EditPredictionDelegate, EditPredictionIconSet}; +use edit_prediction_types::{ + EditPrediction, EditPredictionDelegate, EditPredictionDismissReason, EditPredictionIconSet, +}; use futures::AsyncReadExt; use gpui::{App, Context, Entity, Task}; use http_client::HttpClient; @@ -313,7 +315,7 @@ impl EditPredictionDelegate for CodestralEditPredictionDelegate { self.current_completion = None; } - fn discard(&mut self, _cx: &mut Context) { + fn discard(&mut self, _reason: EditPredictionDismissReason, _cx: &mut Context) { log::debug!("Codestral: Completion discarded"); self.pending_request = None; self.current_completion = None; diff --git a/crates/copilot/src/copilot_edit_prediction_delegate.rs b/crates/copilot/src/copilot_edit_prediction_delegate.rs index 6bc2da37f1799e32532e558c4943232598ee1f5d..49fe5080c27df461597aa1765b11cbe773f277ad 100644 --- a/crates/copilot/src/copilot_edit_prediction_delegate.rs +++ b/crates/copilot/src/copilot_edit_prediction_delegate.rs @@ -7,7 +7,8 @@ use crate::{ }; use anyhow::Result; use edit_prediction_types::{ - EditPrediction, EditPredictionDelegate, EditPredictionIconSet, interpolate_edits, + EditPrediction, EditPredictionDelegate, EditPredictionDismissReason, EditPredictionIconSet, + interpolate_edits, }; use gpui::{App, Context, Entity, Task}; use icons::IconName; @@ -128,7 +129,7 @@ impl EditPredictionDelegate for CopilotEditPredictionDelegate { } } - fn discard(&mut self, _: &mut Context) {} + fn discard(&mut self, _reason: EditPredictionDismissReason, _: &mut Context) {} fn suggest( &mut self, diff --git a/crates/edit_prediction/src/edit_prediction.rs b/crates/edit_prediction/src/edit_prediction.rs index 0ce1b51fb4846a8299452f39b9573ce67652ba73..17204510943a951b14f2ffc4b24917cb7f67b329 100644 --- a/crates/edit_prediction/src/edit_prediction.rs +++ b/crates/edit_prediction/src/edit_prediction.rs @@ -307,11 +307,13 @@ impl ProjectState { return; }; - this.update(cx, |this, _cx| { + this.update(cx, |this, cx| { this.reject_prediction( prediction_id, EditPredictionRejectReason::Canceled, false, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); }) .ok(); @@ -1214,7 +1216,14 @@ impl EditPredictionStore { EditPredictionModel::Sweep => { sweep_ai::edit_prediction_accepted(self, current_prediction, cx) } - EditPredictionModel::Mercury | EditPredictionModel::Ollama => {} + EditPredictionModel::Mercury => { + mercury::edit_prediction_accepted( + current_prediction.prediction.id, + self.client.http_client(), + cx, + ); + } + EditPredictionModel::Ollama => {} EditPredictionModel::Zeta1 | EditPredictionModel::Zeta2 { .. } => { zeta2::edit_prediction_accepted(self, current_prediction, cx) } @@ -1284,11 +1293,19 @@ impl EditPredictionStore { &mut self, reason: EditPredictionRejectReason, project: &Entity, + dismiss_reason: edit_prediction_types::EditPredictionDismissReason, + cx: &App, ) { if let Some(project_state) = self.projects.get_mut(&project.entity_id()) { project_state.pending_predictions.clear(); if let Some(prediction) = project_state.current_prediction.take() { - self.reject_prediction(prediction.prediction.id, reason, prediction.was_shown); + self.reject_prediction( + prediction.prediction.id, + reason, + prediction.was_shown, + dismiss_reason, + cx, + ); } }; } @@ -1347,25 +1364,32 @@ impl EditPredictionStore { prediction_id: EditPredictionId, reason: EditPredictionRejectReason, was_shown: bool, + dismiss_reason: edit_prediction_types::EditPredictionDismissReason, + cx: &App, ) { match self.edit_prediction_model { EditPredictionModel::Zeta1 | EditPredictionModel::Zeta2 { .. } => { - if self.custom_predict_edits_url.is_some() { - return; + if self.custom_predict_edits_url.is_none() { + self.reject_predictions_tx + .unbounded_send(EditPredictionRejection { + request_id: prediction_id.to_string(), + reason, + was_shown, + }) + .log_err(); } } - EditPredictionModel::Sweep - | EditPredictionModel::Mercury - | EditPredictionModel::Ollama => return, + EditPredictionModel::Sweep | EditPredictionModel::Ollama => {} + EditPredictionModel::Mercury => { + mercury::edit_prediction_rejected( + prediction_id, + was_shown, + dismiss_reason, + self.client.http_client(), + cx, + ); + } } - - self.reject_predictions_tx - .unbounded_send(EditPredictionRejection { - request_id: prediction_id.to_string(), - reason, - was_shown, - }) - .log_err(); } fn is_refreshing(&self, project: &Entity) -> bool { @@ -1614,6 +1638,8 @@ impl EditPredictionStore { this.reject_current_prediction( EditPredictionRejectReason::Replaced, &project, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); Some(new_prediction) @@ -1622,6 +1648,8 @@ impl EditPredictionStore { new_prediction.prediction.id, EditPredictionRejectReason::CurrentPreferred, false, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); None } @@ -1630,7 +1658,13 @@ impl EditPredictionStore { } } Err(reject_reason) => { - this.reject_prediction(prediction_result.id, reject_reason, false); + this.reject_prediction( + prediction_result.id, + reject_reason, + false, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, + ); None } } diff --git a/crates/edit_prediction/src/edit_prediction_tests.rs b/crates/edit_prediction/src/edit_prediction_tests.rs index 990373b5f9dedd5dd4c5c07e09a86d28a57c8135..00092c0742d9fff0d6b05542efb82e237924c8d7 100644 --- a/crates/edit_prediction/src/edit_prediction_tests.rs +++ b/crates/edit_prediction/src/edit_prediction_tests.rs @@ -93,8 +93,13 @@ async fn test_current_state(cx: &mut TestAppContext) { assert_matches!(prediction, BufferEditPrediction::Local { .. }); }); - ep_store.update(cx, |ep_store, _cx| { - ep_store.reject_current_prediction(EditPredictionRejectReason::Discarded, &project); + ep_store.update(cx, |ep_store, cx| { + ep_store.reject_current_prediction( + EditPredictionRejectReason::Discarded, + &project, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, + ); }); // Prediction for diagnostic in another file @@ -1125,16 +1130,20 @@ async fn test_cancel_second_on_third_request(cx: &mut TestAppContext) { async fn test_rejections_flushing(cx: &mut TestAppContext) { let (ep_store, mut requests) = init_test_with_fake_client(cx); - ep_store.update(cx, |ep_store, _cx| { + ep_store.update(cx, |ep_store, cx| { ep_store.reject_prediction( EditPredictionId("test-1".into()), EditPredictionRejectReason::Discarded, false, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); ep_store.reject_prediction( EditPredictionId("test-2".into()), EditPredictionRejectReason::Canceled, true, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); }); @@ -1164,12 +1173,14 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) { ); // Reaching batch size limit sends without debounce - ep_store.update(cx, |ep_store, _cx| { + ep_store.update(cx, |ep_store, cx| { for i in 0..70 { ep_store.reject_prediction( EditPredictionId(format!("batch-{}", i).into()), EditPredictionRejectReason::Discarded, false, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); } }); @@ -1195,11 +1206,13 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) { assert_eq!(reject_request.rejections[19].request_id, "batch-69"); // Request failure - ep_store.update(cx, |ep_store, _cx| { + ep_store.update(cx, |ep_store, cx| { ep_store.reject_prediction( EditPredictionId("retry-1".into()), EditPredictionRejectReason::Discarded, false, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); }); @@ -1213,11 +1226,13 @@ async fn test_rejections_flushing(cx: &mut TestAppContext) { drop(_respond_tx); // Add another rejection - ep_store.update(cx, |ep_store, _cx| { + ep_store.update(cx, |ep_store, cx| { ep_store.reject_prediction( EditPredictionId("retry-2".into()), EditPredictionRejectReason::Discarded, false, + edit_prediction_types::EditPredictionDismissReason::Ignored, + cx, ); }); diff --git a/crates/edit_prediction/src/mercury.rs b/crates/edit_prediction/src/mercury.rs index 4396f5ac880bcea15d44bbf987ac50f16a746615..2e0a3b3c13312db4c3fc410c200d65897f28c343 100644 --- a/crates/edit_prediction/src/mercury.rs +++ b/crates/edit_prediction/src/mercury.rs @@ -4,14 +4,18 @@ use crate::{ prediction::EditPredictionResult, zeta1::compute_edits, }; use anyhow::{Context as _, Result}; +use edit_prediction_types::EditPredictionDismissReason; use futures::AsyncReadExt as _; use gpui::{ App, AppContext as _, Entity, Global, SharedString, Task, - http_client::{self, AsyncBody, Method}, + http_client::{self, AsyncBody, HttpClient, Method}, }; use language::{OffsetRangeExt as _, ToOffset, ToPoint as _}; use language_model::{ApiKeyState, EnvVar, env_var}; +use release_channel::AppVersion; +use serde::Serialize; use std::{mem, ops::Range, path::Path, sync::Arc, time::Instant}; + use zeta_prompt::ZetaPromptInput; const MERCURY_API_URL: &str = "https://api.inceptionlabs.ai/v1/edit/completions"; @@ -324,3 +328,92 @@ pub fn load_mercury_api_token(cx: &mut App) -> Task, + cx: &App, +) { + send_feedback(prediction_id, MercuryUserAction::Accept, http_client, cx); +} + +pub(crate) fn edit_prediction_rejected( + prediction_id: EditPredictionId, + was_shown: bool, + dismiss_reason: EditPredictionDismissReason, + http_client: Arc, + cx: &App, +) { + if !was_shown { + return; + } + let action = match dismiss_reason { + EditPredictionDismissReason::Rejected => MercuryUserAction::Reject, + EditPredictionDismissReason::Ignored => MercuryUserAction::Ignore, + }; + send_feedback(prediction_id, action, http_client, cx); +} + +fn send_feedback( + prediction_id: EditPredictionId, + action: MercuryUserAction, + http_client: Arc, + cx: &App, +) { + let request_id = prediction_id.0; + let app_version = AppVersion::global(cx); + cx.background_spawn(async move { + if !request_id.starts_with("cmpl-") { + log::warn!( + "Mercury feedback: invalid request_id '{}' - must start with 'cmpl-'", + request_id + ); + return anyhow::Ok(()); + } + + let body = FeedbackRequest { + request_id, + provider_name: "zed", + user_action: action, + provider_version: app_version.to_string(), + }; + + let request = http_client::Request::builder() + .uri(FEEDBACK_API_URL) + .method(Method::POST) + .header("Content-Type", "application/json") + .body(AsyncBody::from(serde_json::to_vec(&body)?))?; + + let response = http_client.send(request).await?; + if !response.status().is_success() { + anyhow::bail!("Feedback API returned status: {}", response.status()); + } + + log::debug!( + "Mercury feedback sent: request_id={}, action={:?}", + body.request_id, + body.user_action + ); + + anyhow::Ok(()) + }) + .detach_and_log_err(cx); +} diff --git a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs index cf7ecfb0a64952a8041aa1c463893b06414bbadf..9a27b38281a5a21b17750406ac9ff38745158440 100644 --- a/crates/edit_prediction/src/zed_edit_prediction_delegate.rs +++ b/crates/edit_prediction/src/zed_edit_prediction_delegate.rs @@ -3,7 +3,8 @@ use std::{cmp, sync::Arc}; use client::{Client, UserStore}; use cloud_llm_client::EditPredictionRejectReason; use edit_prediction_types::{ - DataCollectionState, EditPredictionDelegate, EditPredictionIconSet, SuggestionDisplayType, + DataCollectionState, EditPredictionDelegate, EditPredictionDismissReason, + EditPredictionIconSet, SuggestionDisplayType, }; use gpui::{App, Entity, prelude::*}; use language::{Buffer, ToPoint as _}; @@ -167,9 +168,14 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { }); } - fn discard(&mut self, cx: &mut Context) { - self.store.update(cx, |store, _cx| { - store.reject_current_prediction(EditPredictionRejectReason::Discarded, &self.project); + fn discard(&mut self, reason: EditPredictionDismissReason, cx: &mut Context) { + self.store.update(cx, |store, cx| { + store.reject_current_prediction( + EditPredictionRejectReason::Discarded, + &self.project, + reason, + cx, + ); }); } @@ -207,6 +213,8 @@ impl EditPredictionDelegate for ZedEditPredictionDelegate { store.reject_current_prediction( EditPredictionRejectReason::InterpolatedEmpty, &self.project, + EditPredictionDismissReason::Ignored, + cx, ); return None; }; diff --git a/crates/edit_prediction_types/src/edit_prediction_types.rs b/crates/edit_prediction_types/src/edit_prediction_types.rs index a1761660fd09f516b4c3947d5416b95d3c8e1041..0879a055270360ed37b71c14a53e7178def81f81 100644 --- a/crates/edit_prediction_types/src/edit_prediction_types.rs +++ b/crates/edit_prediction_types/src/edit_prediction_types.rs @@ -2,6 +2,12 @@ use std::{ops::Range, sync::Arc}; use client::EditPredictionUsage; use gpui::{App, Context, Entity, SharedString}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq)] +pub enum EditPredictionDismissReason { + Rejected, + Ignored, +} use icons::IconName; use language::{Anchor, Buffer, OffsetRangeExt}; @@ -178,7 +184,7 @@ pub trait EditPredictionDelegate: 'static + Sized { cx: &mut Context, ); fn accept(&mut self, cx: &mut Context); - fn discard(&mut self, cx: &mut Context); + fn discard(&mut self, reason: EditPredictionDismissReason, cx: &mut Context); fn did_show(&mut self, _display_type: SuggestionDisplayType, _cx: &mut Context) {} fn suggest( &mut self, @@ -214,7 +220,7 @@ pub trait EditPredictionDelegateHandle { ); fn did_show(&self, display_type: SuggestionDisplayType, cx: &mut App); fn accept(&self, cx: &mut App); - fn discard(&self, cx: &mut App); + fn discard(&self, reason: EditPredictionDismissReason, cx: &mut App); fn suggest( &self, buffer: &Entity, @@ -292,8 +298,8 @@ where self.update(cx, |this, cx| this.accept(cx)) } - fn discard(&self, cx: &mut App) { - self.update(cx, |this, cx| this.discard(cx)) + fn discard(&self, reason: EditPredictionDismissReason, cx: &mut App) { + self.update(cx, |this, cx| this.discard(reason, cx)) } fn did_show(&self, display_type: SuggestionDisplayType, cx: &mut App) { diff --git a/crates/editor/src/edit_prediction_tests.rs b/crates/editor/src/edit_prediction_tests.rs index cc9c32fed7c2844b60c9aaaab9cd94ef534ee242..26ccf8d6a166f4771814d70e70365dfb668bc7f6 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -623,7 +623,12 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate { fn accept(&mut self, _cx: &mut gpui::Context) {} - fn discard(&mut self, _cx: &mut gpui::Context) {} + fn discard( + &mut self, + _reason: edit_prediction_types::EditPredictionDismissReason, + _cx: &mut gpui::Context, + ) { + } fn suggest<'a>( &mut self, @@ -694,7 +699,12 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate { fn accept(&mut self, _cx: &mut gpui::Context) {} - fn discard(&mut self, _cx: &mut gpui::Context) {} + fn discard( + &mut self, + _reason: edit_prediction_types::EditPredictionDismissReason, + _cx: &mut gpui::Context, + ) { + } fn suggest<'a>( &mut self, diff --git a/crates/editor/src/editor.rs b/crates/editor/src/editor.rs index 354c4b896b3ca51c6d9321e17f277d333f1b399e..6a819ce27eaa4a76957f9b09a6d1ae7a8c0a5284 100644 --- a/crates/editor/src/editor.rs +++ b/crates/editor/src/editor.rs @@ -92,8 +92,8 @@ use convert_case::{Case, Casing}; use dap::TelemetrySpawnLocation; use display_map::*; use edit_prediction_types::{ - EditPredictionDelegate, EditPredictionDelegateHandle, EditPredictionGranularity, - SuggestionDisplayType, + EditPredictionDelegate, EditPredictionDelegateHandle, EditPredictionDismissReason, + EditPredictionGranularity, SuggestionDisplayType, }; use editor_settings::{GoToDefinitionFallback, Minimap as MinimapSettings}; use element::{AcceptEditPredictionBinding, LineWithInvisibles, PositionMap, layout_line}; @@ -8057,7 +8057,12 @@ impl Editor { } if let Some(provider) = self.edit_prediction_provider() { - provider.discard(cx); + let reason = if should_report_edit_prediction_event { + EditPredictionDismissReason::Rejected + } else { + EditPredictionDismissReason::Ignored + }; + provider.discard(reason, cx); } self.take_active_edit_prediction(cx) diff --git a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs b/crates/supermaven/src/supermaven_edit_prediction_delegate.rs index f97fe9edfe0408f464caf2a6b5d5ed652e4c5db1..b3b4d42c7549c30607f0c3ff6a3fa068fb24ff2f 100644 --- a/crates/supermaven/src/supermaven_edit_prediction_delegate.rs +++ b/crates/supermaven/src/supermaven_edit_prediction_delegate.rs @@ -1,6 +1,8 @@ use crate::{Supermaven, SupermavenCompletionStateId}; use anyhow::Result; -use edit_prediction_types::{EditPrediction, EditPredictionDelegate, EditPredictionIconSet}; +use edit_prediction_types::{ + EditPrediction, EditPredictionDelegate, EditPredictionDismissReason, EditPredictionIconSet, +}; use futures::StreamExt as _; use gpui::{App, Context, Entity, EntityId, Task}; use language::{Anchor, Buffer, BufferSnapshot}; @@ -201,7 +203,7 @@ impl EditPredictionDelegate for SupermavenEditPredictionDelegate { reset_completion_cache(self, _cx); } - fn discard(&mut self, _cx: &mut Context) { + fn discard(&mut self, _reason: EditPredictionDismissReason, _cx: &mut Context) { reset_completion_cache(self, _cx); }