From 14835b0ef3a85e5cb20e9137a3bec94081b35009 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:54 +0000 Subject: [PATCH] edit_prediction: Add Mercury accept/reject tracking (#48306) (cherry-pick to stable) (#48403) MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Cherry-pick of #48306 to stable ---- ### 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 270526f0e323d0792167a8538baebe193a852e08..8bf2f7b2095bc69475b24a03eed801527127a852 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 c7a7963de26564df81e91c4602c19f8ccc437179..b744cd8c562b646aea51a035a951e66fafe1019a 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 5d49dd18524ddc95ace929982ad90fc5d2f70fdf..37a91c9cafa323a2f0f74668caff5cf45394377f 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 1291d23a80896e53f2a4d2ceaa595fd26b39b949..7f42dfca78ec110b8c6d00e7dc0534a8b71499db 100644 --- a/crates/edit_prediction/src/edit_prediction_tests.rs +++ b/crates/edit_prediction/src/edit_prediction_tests.rs @@ -92,8 +92,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 @@ -1124,16 +1129,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, ); }); @@ -1163,12 +1172,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, ); } }); @@ -1194,11 +1205,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, ); }); @@ -1212,11 +1225,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 09d301c5fa2e7c0edf964c626d3d40d9764c33cc..66302787aa8ea5b4b58c55e1b0cdd80a24510550 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"; @@ -321,3 +325,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 02c91ad159dd824c6ac7ed00df483cfbb0bd836e..bd57c9cd032e206e70991b597ede79b4e146d695 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 80e62a2b0d579f3accaec6db22f0a435d9a73ca1..b697248493422d5d9128c7d17b99ffd8af82b48f 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}; @@ -177,7 +183,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, @@ -213,7 +219,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, @@ -291,8 +297,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 798860d0aa201733f7852808405f261cf38b9112..256467af1f8ead311af2488869f7725951a83e6b 100644 --- a/crates/editor/src/edit_prediction_tests.rs +++ b/crates/editor/src/edit_prediction_tests.rs @@ -492,7 +492,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, @@ -563,7 +568,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 b9c9cc3e80a9631fb1f88513650eaeed75d92e2b..ab93fc203a6479459500b128eeb9d802a0dab659 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}; @@ -8021,7 +8021,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 248177d577284f43ee9959887651b496c8492770..d338609dd80cc8a1db262c0dd46c20ea74543f37 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}; @@ -200,7 +202,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); }