Detailed changes
@@ -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<Self>) {
+ fn discard(&mut self, _reason: EditPredictionDismissReason, _cx: &mut Context<Self>) {
log::debug!("Codestral: Completion discarded");
self.pending_request = None;
self.current_completion = None;
@@ -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<Self>) {}
+ fn discard(&mut self, _reason: EditPredictionDismissReason, _: &mut Context<Self>) {}
fn suggest(
&mut self,
@@ -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<Project>,
+ 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<Project>) -> 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
}
}
@@ -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,
);
});
@@ -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<Result<(), language_model::A
key_state.load_if_needed(MERCURY_CREDENTIALS_URL, |s| s, cx)
})
}
+
+const FEEDBACK_API_URL: &str = "https://api-feedback.inceptionlabs.ai/feedback";
+
+#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
+#[serde(rename_all = "snake_case")]
+enum MercuryUserAction {
+ Accept,
+ Reject,
+ Ignore,
+}
+
+#[derive(Serialize)]
+struct FeedbackRequest {
+ request_id: SharedString,
+ provider_name: &'static str,
+ user_action: MercuryUserAction,
+ provider_version: String,
+}
+
+pub(crate) fn edit_prediction_accepted(
+ prediction_id: EditPredictionId,
+ http_client: Arc<dyn HttpClient>,
+ 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<dyn HttpClient>,
+ 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<dyn HttpClient>,
+ 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);
+}
@@ -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>) {
- self.store.update(cx, |store, _cx| {
- store.reject_current_prediction(EditPredictionRejectReason::Discarded, &self.project);
+ fn discard(&mut self, reason: EditPredictionDismissReason, cx: &mut Context<Self>) {
+ 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;
};
@@ -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<Self>,
);
fn accept(&mut self, cx: &mut Context<Self>);
- fn discard(&mut self, cx: &mut Context<Self>);
+ fn discard(&mut self, reason: EditPredictionDismissReason, cx: &mut Context<Self>);
fn did_show(&mut self, _display_type: SuggestionDisplayType, _cx: &mut Context<Self>) {}
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<Buffer>,
@@ -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) {
@@ -623,7 +623,12 @@ impl EditPredictionDelegate for FakeEditPredictionDelegate {
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
- fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
+ fn discard(
+ &mut self,
+ _reason: edit_prediction_types::EditPredictionDismissReason,
+ _cx: &mut gpui::Context<Self>,
+ ) {
+ }
fn suggest<'a>(
&mut self,
@@ -694,7 +699,12 @@ impl EditPredictionDelegate for FakeNonZedEditPredictionDelegate {
fn accept(&mut self, _cx: &mut gpui::Context<Self>) {}
- fn discard(&mut self, _cx: &mut gpui::Context<Self>) {}
+ fn discard(
+ &mut self,
+ _reason: edit_prediction_types::EditPredictionDismissReason,
+ _cx: &mut gpui::Context<Self>,
+ ) {
+ }
fn suggest<'a>(
&mut self,
@@ -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)
@@ -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<Self>) {
+ fn discard(&mut self, _reason: EditPredictionDismissReason, _cx: &mut Context<Self>) {
reset_completion_cache(self, _cx);
}