Cargo.lock 🔗
@@ -21533,6 +21533,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "telemetry",
"text",
"ui",
"ui_input",
Agus Zubiaga created
Adds a way to submit feedback about a zeta2 prediction from the
inspector. The telemetry event includes:
- project snapshot (git + unsaved buffer state)
- the full request and response
- user feedback kind and text
Release Notes:
- N/A
Cargo.lock | 1
assets/keymaps/default-linux.json | 8
assets/keymaps/default-macos.json | 8
assets/keymaps/default-windows.json | 8
crates/agent/src/agent.rs | 16
crates/agent/src/thread.rs | 99 -----
crates/project/src/project.rs | 1
crates/project/src/telemetry_snapshot.rs | 125 +++++++
crates/zeta2/src/zeta2.rs | 33
crates/zeta2_tools/Cargo.toml | 1
crates/zeta2_tools/src/zeta2_tools.rs | 459 ++++++++++++++++++++-----
11 files changed, 540 insertions(+), 219 deletions(-)
@@ -21533,6 +21533,7 @@ dependencies = [
"serde",
"serde_json",
"settings",
+ "telemetry",
"text",
"ui",
"ui_input",
@@ -1290,5 +1290,13 @@
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
+ },
+ {
+ "context": "Zeta2Feedback > Editor",
+ "bindings": {
+ "enter": "editor::Newline",
+ "ctrl-enter up": "dev::Zeta2RatePredictionPositive",
+ "ctrl-enter down": "dev::Zeta2RatePredictionNegative"
+ }
}
]
@@ -1396,5 +1396,13 @@
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
+ },
+ {
+ "context": "Zeta2Feedback > Editor",
+ "bindings": {
+ "enter": "editor::Newline",
+ "cmd-enter up": "dev::Zeta2RatePredictionPositive",
+ "cmd-enter down": "dev::Zeta2RatePredictionNegative"
+ }
}
]
@@ -1319,5 +1319,13 @@
"home": "settings_editor::FocusFirstNavEntry",
"end": "settings_editor::FocusLastNavEntry"
}
+ },
+ {
+ "context": "Zeta2Feedback > Editor",
+ "bindings": {
+ "enter": "editor::Newline",
+ "ctrl-enter up": "dev::Zeta2RatePredictionPositive",
+ "ctrl-enter down": "dev::Zeta2RatePredictionNegative"
+ }
}
]
@@ -48,24 +48,10 @@ use util::rel_path::RelPath;
#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
pub struct ProjectSnapshot {
- pub worktree_snapshots: Vec<WorktreeSnapshot>,
+ pub worktree_snapshots: Vec<project::telemetry_snapshot::TelemetryWorktreeSnapshot>,
pub timestamp: DateTime<Utc>,
}
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct WorktreeSnapshot {
- pub worktree_path: String,
- pub git_state: Option<GitState>,
-}
-
-#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
-pub struct GitState {
- pub remote_url: Option<String>,
- pub head_sha: Option<String>,
- pub current_branch: Option<String>,
- pub diff: Option<String>,
-}
-
const RULES_FILE_NAMES: [&str; 9] = [
".rules",
".cursorrules",
@@ -1,9 +1,8 @@
use crate::{
ContextServerRegistry, CopyPathTool, CreateDirectoryTool, DbLanguageModel, DbThread,
- DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GitState, GrepTool,
+ DeletePathTool, DiagnosticsTool, EditFileTool, FetchTool, FindPathTool, GrepTool,
ListDirectoryTool, MovePathTool, NowTool, OpenTool, ProjectSnapshot, ReadFileTool,
SystemPromptTemplate, Template, Templates, TerminalTool, ThinkingTool, WebSearchTool,
- WorktreeSnapshot,
};
use acp_thread::{MentionUri, UserMessageId};
use action_log::ActionLog;
@@ -26,7 +25,6 @@ use futures::{
future::Shared,
stream::FuturesUnordered,
};
-use git::repository::DiffType;
use gpui::{
App, AppContext, AsyncApp, Context, Entity, EventEmitter, SharedString, Task, WeakEntity,
};
@@ -37,10 +35,7 @@ use language_model::{
LanguageModelToolResultContent, LanguageModelToolSchemaFormat, LanguageModelToolUse,
LanguageModelToolUseId, Role, SelectedModel, StopReason, TokenUsage, ZED_CLOUD_PROVIDER_ID,
};
-use project::{
- Project,
- git_store::{GitStore, RepositoryState},
-};
+use project::Project;
use prompt_store::ProjectContext;
use schemars::{JsonSchema, Schema};
use serde::{Deserialize, Serialize};
@@ -880,101 +875,17 @@ impl Thread {
project: Entity<Project>,
cx: &mut Context<Self>,
) -> Task<Arc<ProjectSnapshot>> {
- let git_store = project.read(cx).git_store().clone();
- let worktree_snapshots: Vec<_> = project
- .read(cx)
- .visible_worktrees(cx)
- .map(|worktree| Self::worktree_snapshot(worktree, git_store.clone(), cx))
- .collect();
-
+ let task = project::telemetry_snapshot::TelemetrySnapshot::new(&project, cx);
cx.spawn(async move |_, _| {
- let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
+ let snapshot = task.await;
Arc::new(ProjectSnapshot {
- worktree_snapshots,
+ worktree_snapshots: snapshot.worktree_snapshots,
timestamp: Utc::now(),
})
})
}
- fn worktree_snapshot(
- worktree: Entity<project::Worktree>,
- git_store: Entity<GitStore>,
- cx: &App,
- ) -> Task<WorktreeSnapshot> {
- cx.spawn(async move |cx| {
- // Get worktree path and snapshot
- let worktree_info = cx.update(|app_cx| {
- let worktree = worktree.read(app_cx);
- let path = worktree.abs_path().to_string_lossy().into_owned();
- let snapshot = worktree.snapshot();
- (path, snapshot)
- });
-
- let Ok((worktree_path, _snapshot)) = worktree_info else {
- return WorktreeSnapshot {
- worktree_path: String::new(),
- git_state: None,
- };
- };
-
- let git_state = git_store
- .update(cx, |git_store, cx| {
- git_store
- .repositories()
- .values()
- .find(|repo| {
- repo.read(cx)
- .abs_path_to_repo_path(&worktree.read(cx).abs_path())
- .is_some()
- })
- .cloned()
- })
- .ok()
- .flatten()
- .map(|repo| {
- repo.update(cx, |repo, _| {
- let current_branch =
- repo.branch.as_ref().map(|branch| branch.name().to_owned());
- repo.send_job(None, |state, _| async move {
- let RepositoryState::Local { backend, .. } = state else {
- return GitState {
- remote_url: None,
- head_sha: None,
- current_branch,
- diff: None,
- };
- };
-
- let remote_url = backend.remote_url("origin");
- let head_sha = backend.head_sha().await;
- let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
-
- GitState {
- remote_url,
- head_sha,
- current_branch,
- diff,
- }
- })
- })
- });
-
- let git_state = match git_state {
- Some(git_state) => match git_state.ok() {
- Some(git_state) => git_state.await.ok(),
- None => None,
- },
- None => None,
- };
-
- WorktreeSnapshot {
- worktree_path,
- git_state,
- }
- })
- }
-
pub fn project_context(&self) -> &Entity<ProjectContext> {
&self.project_context
}
@@ -16,6 +16,7 @@ pub mod project_settings;
pub mod search;
mod task_inventory;
pub mod task_store;
+pub mod telemetry_snapshot;
pub mod terminals;
pub mod toolchain_store;
pub mod worktree_store;
@@ -0,0 +1,125 @@
+use git::repository::DiffType;
+use gpui::{App, Entity, Task};
+use serde::{Deserialize, Serialize};
+use worktree::Worktree;
+
+use crate::{
+ Project,
+ git_store::{GitStore, RepositoryState},
+};
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct TelemetrySnapshot {
+ pub worktree_snapshots: Vec<TelemetryWorktreeSnapshot>,
+}
+
+impl TelemetrySnapshot {
+ pub fn new(project: &Entity<Project>, cx: &mut App) -> Task<TelemetrySnapshot> {
+ let git_store = project.read(cx).git_store().clone();
+ let worktree_snapshots: Vec<_> = project
+ .read(cx)
+ .visible_worktrees(cx)
+ .map(|worktree| TelemetryWorktreeSnapshot::new(worktree, git_store.clone(), cx))
+ .collect();
+
+ cx.spawn(async move |_| {
+ let worktree_snapshots = futures::future::join_all(worktree_snapshots).await;
+
+ Self { worktree_snapshots }
+ })
+ }
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct TelemetryWorktreeSnapshot {
+ pub worktree_path: String,
+ pub git_state: Option<GitState>,
+}
+
+#[derive(Debug, Clone, Serialize, Deserialize, PartialEq)]
+pub struct GitState {
+ pub remote_url: Option<String>,
+ pub head_sha: Option<String>,
+ pub current_branch: Option<String>,
+ pub diff: Option<String>,
+}
+
+impl TelemetryWorktreeSnapshot {
+ fn new(
+ worktree: Entity<Worktree>,
+ git_store: Entity<GitStore>,
+ cx: &App,
+ ) -> Task<TelemetryWorktreeSnapshot> {
+ cx.spawn(async move |cx| {
+ // Get worktree path and snapshot
+ let worktree_info = cx.update(|app_cx| {
+ let worktree = worktree.read(app_cx);
+ let path = worktree.abs_path().to_string_lossy().into_owned();
+ let snapshot = worktree.snapshot();
+ (path, snapshot)
+ });
+
+ let Ok((worktree_path, _snapshot)) = worktree_info else {
+ return TelemetryWorktreeSnapshot {
+ worktree_path: String::new(),
+ git_state: None,
+ };
+ };
+
+ let git_state = git_store
+ .update(cx, |git_store, cx| {
+ git_store
+ .repositories()
+ .values()
+ .find(|repo| {
+ repo.read(cx)
+ .abs_path_to_repo_path(&worktree.read(cx).abs_path())
+ .is_some()
+ })
+ .cloned()
+ })
+ .ok()
+ .flatten()
+ .map(|repo| {
+ repo.update(cx, |repo, _| {
+ let current_branch =
+ repo.branch.as_ref().map(|branch| branch.name().to_owned());
+ repo.send_job(None, |state, _| async move {
+ let RepositoryState::Local { backend, .. } = state else {
+ return GitState {
+ remote_url: None,
+ head_sha: None,
+ current_branch,
+ diff: None,
+ };
+ };
+
+ let remote_url = backend.remote_url("origin");
+ let head_sha = backend.head_sha().await;
+ let diff = backend.diff(DiffType::HeadToWorktree).await.ok();
+
+ GitState {
+ remote_url,
+ head_sha,
+ current_branch,
+ diff,
+ }
+ })
+ })
+ });
+
+ let git_state = match git_state {
+ Some(git_state) => match git_state.ok() {
+ Some(git_state) => git_state.await.ok(),
+ None => None,
+ },
+ None => None,
+ };
+
+ TelemetryWorktreeSnapshot {
+ worktree_path,
+ git_state,
+ }
+ })
+ }
+}
@@ -11,7 +11,7 @@ use edit_prediction_context::{
DeclarationId, DeclarationStyle, EditPredictionContext, EditPredictionContextOptions,
EditPredictionExcerptOptions, EditPredictionScoreOptions, SyntaxIndex, SyntaxIndexState,
};
-use feature_flags::FeatureFlag;
+use feature_flags::{FeatureFlag, FeatureFlagAppExt as _};
use futures::AsyncReadExt as _;
use futures::channel::{mpsc, oneshot};
use gpui::http_client::{AsyncBody, Method};
@@ -32,7 +32,6 @@ use std::sync::Arc;
use std::time::{Duration, Instant};
use thiserror::Error;
use util::rel_path::RelPathBuf;
-use util::some_or_debug_panic;
use workspace::notifications::{ErrorMessagePrompt, NotificationId, show_app_notification};
mod prediction;
@@ -103,12 +102,12 @@ pub struct ZetaOptions {
}
pub struct PredictionDebugInfo {
- pub context: EditPredictionContext,
+ pub request: predict_edits_v3::PredictEditsRequest,
pub retrieval_time: TimeDelta,
pub buffer: WeakEntity<Buffer>,
pub position: language::Anchor,
pub local_prompt: Result<String, String>,
- pub response_rx: oneshot::Receiver<Result<RequestDebugInfo, String>>,
+ pub response_rx: oneshot::Receiver<Result<predict_edits_v3::PredictEditsResponse, String>>,
}
pub type RequestDebugInfo = predict_edits_v3::DebugInfo;
@@ -571,6 +570,9 @@ impl Zeta {
if path.pop() { Some(path) } else { None }
});
+ // TODO data collection
+ let can_collect_data = cx.is_staff();
+
let request_task = cx.background_spawn({
let snapshot = snapshot.clone();
let buffer = buffer.clone();
@@ -606,25 +608,22 @@ impl Zeta {
options.max_diagnostic_bytes,
);
- let debug_context = debug_tx.map(|tx| (tx, context.clone()));
-
let request = make_cloud_request(
excerpt_path,
context,
events,
- // TODO data collection
- false,
+ can_collect_data,
diagnostic_groups,
diagnostic_groups_truncated,
None,
- debug_context.is_some(),
+ debug_tx.is_some(),
&worktree_snapshots,
index_state.as_deref(),
Some(options.max_prompt_bytes),
options.prompt_format,
);
- let debug_response_tx = if let Some((debug_tx, context)) = debug_context {
+ let debug_response_tx = if let Some(debug_tx) = &debug_tx {
let (response_tx, response_rx) = oneshot::channel();
let local_prompt = PlannedPrompt::populate(&request)
@@ -633,7 +632,7 @@ impl Zeta {
debug_tx
.unbounded_send(PredictionDebugInfo {
- context,
+ request: request.clone(),
retrieval_time,
buffer: buffer.downgrade(),
local_prompt,
@@ -660,12 +659,12 @@ impl Zeta {
if let Some(debug_response_tx) = debug_response_tx {
debug_response_tx
- .send(response.as_ref().map_err(|err| err.to_string()).and_then(
- |response| match some_or_debug_panic(response.0.debug_info.clone()) {
- Some(debug_info) => Ok(debug_info),
- None => Err("Missing debug info".to_string()),
- },
- ))
+ .send(
+ response
+ .as_ref()
+ .map_err(|err| err.to_string())
+ .map(|response| response.0.clone()),
+ )
.ok();
}
@@ -27,6 +27,7 @@ multi_buffer.workspace = true
ordered-float.workspace = true
project.workspace = true
serde.workspace = true
+telemetry.workspace = true
text.workspace = true
ui.workspace = true
ui_input.workspace = true
@@ -1,37 +1,38 @@
-use std::{
- cmp::Reverse, collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc,
- time::Duration,
-};
+use std::{cmp::Reverse, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
use chrono::TimeDelta;
use client::{Client, UserStore};
-use cloud_llm_client::predict_edits_v3::{DeclarationScoreComponents, PromptFormat};
+use cloud_llm_client::predict_edits_v3::{
+ self, DeclarationScoreComponents, PredictEditsRequest, PredictEditsResponse, PromptFormat,
+};
use collections::HashMap;
use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
use feature_flags::FeatureFlagAppExt as _;
-use futures::{StreamExt as _, channel::oneshot};
+use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared};
use gpui::{
- CursorStyle, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity,
- actions, prelude::*,
+ CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
+ WeakEntity, actions, prelude::*,
};
use language::{Buffer, DiskState};
use ordered_float::OrderedFloat;
-use project::{Project, WorktreeId};
-use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*};
+use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot};
+use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
use ui_input::SingleLineInput;
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
use workspace::{Item, SplitDirection, Workspace};
use zeta2::{PredictionDebugInfo, Zeta, Zeta2FeatureFlag, ZetaOptions};
-use edit_prediction_context::{
- DeclarationStyle, EditPredictionContextOptions, EditPredictionExcerptOptions,
-};
+use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
actions!(
dev,
[
/// Opens the language server protocol logs viewer.
- OpenZeta2Inspector
+ OpenZeta2Inspector,
+ /// Rate prediction as positive.
+ Zeta2RatePredictionPositive,
+ /// Rate prediction as negative.
+ Zeta2RatePredictionNegative,
]
);
@@ -89,16 +90,24 @@ struct LastPrediction {
buffer: WeakEntity<Buffer>,
position: language::Anchor,
state: LastPredictionState,
+ request: PredictEditsRequest,
+ project_snapshot: Shared<Task<Arc<TelemetrySnapshot>>>,
_task: Option<Task<()>>,
}
+#[derive(Clone, Copy, PartialEq)]
+enum Feedback {
+ Positive,
+ Negative,
+}
+
enum LastPredictionState {
Requested,
Success {
- inference_time: TimeDelta,
- parsing_time: TimeDelta,
- prompt_planning_time: TimeDelta,
model_response_editor: Entity<Editor>,
+ feedback_editor: Entity<Editor>,
+ feedback: Option<Feedback>,
+ response: predict_edits_v3::PredictEditsResponse,
},
Failed {
message: String,
@@ -129,7 +138,7 @@ impl Zeta2Inspector {
focus_handle: cx.focus_handle(),
project: project.clone(),
last_prediction: None,
- active_view: ActiveView::Context,
+ active_view: ActiveView::Inference,
max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
@@ -300,17 +309,23 @@ impl Zeta2Inspector {
let language_registry = self.project.read(cx).languages().clone();
async move |this, cx| {
let mut languages = HashMap::default();
- for lang_id in prediction
- .context
- .declarations
+ for ext in prediction
+ .request
+ .referenced_declarations
.iter()
- .map(|snippet| snippet.declaration.identifier().language_id)
- .chain(prediction.context.excerpt_text.language_id)
+ .filter_map(|snippet| snippet.path.extension())
+ .chain(prediction.request.excerpt_path.extension())
{
- if let Entry::Vacant(entry) = languages.entry(lang_id) {
+ if !languages.contains_key(ext) {
// Most snippets are gonna be the same language,
// so we think it's fine to do this sequentially for now
- entry.insert(language_registry.language_for_id(lang_id).await.ok());
+ languages.insert(
+ ext.to_owned(),
+ language_registry
+ .language_for_name_or_extension(&ext.to_string_lossy())
+ .await
+ .ok(),
+ );
}
}
@@ -333,13 +348,12 @@ impl Zeta2Inspector {
let excerpt_buffer = cx.new(|cx| {
let mut buffer =
- Buffer::local(prediction.context.excerpt_text.body, cx);
+ Buffer::local(prediction.request.excerpt.clone(), cx);
if let Some(language) = prediction
- .context
- .excerpt_text
- .language_id
- .as_ref()
- .and_then(|id| languages.get(id))
+ .request
+ .excerpt_path
+ .extension()
+ .and_then(|ext| languages.get(ext))
{
buffer.set_language(language.clone(), cx);
}
@@ -353,25 +367,18 @@ impl Zeta2Inspector {
cx,
);
- let mut declarations = prediction.context.declarations.clone();
+ let mut declarations =
+ prediction.request.referenced_declarations.clone();
declarations.sort_unstable_by_key(|declaration| {
- Reverse(OrderedFloat(
- declaration.score(DeclarationStyle::Declaration),
- ))
+ Reverse(OrderedFloat(declaration.declaration_score))
});
for snippet in &declarations {
- let path = this
- .project
- .read(cx)
- .path_for_entry(snippet.declaration.project_entry_id(), cx);
-
let snippet_file = Arc::new(ExcerptMetadataFile {
title: RelPath::unix(&format!(
"{} (Score: {})",
- path.map(|p| p.path.display(path_style).to_string())
- .unwrap_or_else(|| "".to_string()),
- snippet.score(DeclarationStyle::Declaration)
+ snippet.path.display(),
+ snippet.declaration_score
))
.unwrap()
.into(),
@@ -380,11 +387,10 @@ impl Zeta2Inspector {
});
let excerpt_buffer = cx.new(|cx| {
- let mut buffer =
- Buffer::local(snippet.declaration.item_text().0, cx);
+ let mut buffer = Buffer::local(snippet.text.clone(), cx);
buffer.file_updated(snippet_file, cx);
- if let Some(language) =
- languages.get(&snippet.declaration.identifier().language_id)
+ if let Some(ext) = snippet.path.extension()
+ && let Some(language) = languages.get(ext)
{
buffer.set_language(language.clone(), cx);
}
@@ -399,7 +405,7 @@ impl Zeta2Inspector {
let excerpt_id = excerpt_ids.first().unwrap();
excerpt_score_components
- .insert(*excerpt_id, snippet.components.clone());
+ .insert(*excerpt_id, snippet.score_components.clone());
}
multibuffer
@@ -431,25 +437,91 @@ impl Zeta2Inspector {
if let Some(prediction) = this.last_prediction.as_mut() {
prediction.state = match response {
Ok(Ok(response)) => {
- prediction.prompt_editor.update(
- cx,
- |prompt_editor, cx| {
- prompt_editor.set_text(
- response.prompt,
- window,
+ if let Some(debug_info) = &response.debug_info {
+ prediction.prompt_editor.update(
+ cx,
+ |prompt_editor, cx| {
+ prompt_editor.set_text(
+ debug_info.prompt.as_str(),
+ window,
+ cx,
+ );
+ },
+ );
+ }
+
+ let feedback_editor = cx.new(|cx| {
+ let buffer = cx.new(|cx| {
+ let mut buffer = Buffer::local("", cx);
+ buffer.set_language(
+ markdown_language.clone(),
cx,
);
+ buffer
+ });
+ let buffer =
+ cx.new(|cx| MultiBuffer::singleton(buffer, cx));
+ let mut editor = Editor::new(
+ EditorMode::AutoHeight {
+ min_lines: 3,
+ max_lines: None,
+ },
+ buffer,
+ None,
+ window,
+ cx,
+ );
+ editor.set_placeholder_text(
+ "Write feedback here",
+ window,
+ cx,
+ );
+ editor.set_show_line_numbers(false, cx);
+ editor.set_show_gutter(false, cx);
+ editor.set_show_scrollbars(false, cx);
+ editor
+ });
+
+ cx.subscribe_in(
+ &feedback_editor,
+ window,
+ |this, editor, ev, window, cx| match ev {
+ EditorEvent::BufferEdited => {
+ if let Some(last_prediction) =
+ this.last_prediction.as_mut()
+ && let LastPredictionState::Success {
+ feedback: feedback_state,
+ ..
+ } = &mut last_prediction.state
+ {
+ if feedback_state.take().is_some() {
+ editor.update(cx, |editor, cx| {
+ editor.set_placeholder_text(
+ "Write feedback here",
+ window,
+ cx,
+ );
+ });
+ cx.notify();
+ }
+ }
+ }
+ _ => {}
},
- );
+ )
+ .detach();
LastPredictionState::Success {
- prompt_planning_time: response.prompt_planning_time,
- inference_time: response.inference_time,
- parsing_time: response.parsing_time,
model_response_editor: cx.new(|cx| {
let buffer = cx.new(|cx| {
let mut buffer = Buffer::local(
- response.model_response,
+ response
+ .debug_info
+ .as_ref()
+ .map(|p| p.model_response.as_str())
+ .unwrap_or(
+ "(Debug info not available)",
+ ),
cx,
);
buffer.set_language(markdown_language, cx);
@@ -471,6 +543,9 @@ impl Zeta2Inspector {
editor.set_show_scrollbars(false, cx);
editor
}),
+ feedback_editor,
+ feedback: None,
+ response,
}
}
Ok(Err(err)) => {
@@ -486,6 +561,8 @@ impl Zeta2Inspector {
}
});
+ let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
+
this.last_prediction = Some(LastPrediction {
context_editor,
prompt_editor: cx.new(|cx| {
@@ -508,6 +585,11 @@ impl Zeta2Inspector {
buffer,
position,
state: LastPredictionState::Requested,
+ project_snapshot: cx
+ .foreground_executor()
+ .spawn(async move { Arc::new(project_snapshot_task.await) })
+ .shared(),
+ request: prediction.request,
_task: Some(task),
});
cx.notify();
@@ -517,6 +599,103 @@ impl Zeta2Inspector {
});
}
+ fn handle_rate_positive(
+ &mut self,
+ _action: &Zeta2RatePredictionPositive,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.handle_rate(Feedback::Positive, window, cx);
+ }
+
+ fn handle_rate_negative(
+ &mut self,
+ _action: &Zeta2RatePredictionNegative,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) {
+ self.handle_rate(Feedback::Negative, window, cx);
+ }
+
+ fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
+ let Some(last_prediction) = self.last_prediction.as_mut() else {
+ return;
+ };
+ if !last_prediction.request.can_collect_data {
+ return;
+ }
+
+ let project_snapshot_task = last_prediction.project_snapshot.clone();
+
+ cx.spawn_in(window, async move |this, cx| {
+ let project_snapshot = project_snapshot_task.await;
+ this.update_in(cx, |this, window, cx| {
+ let Some(last_prediction) = this.last_prediction.as_mut() else {
+ return;
+ };
+
+ let LastPredictionState::Success {
+ feedback: feedback_state,
+ feedback_editor,
+ model_response_editor,
+ response,
+ ..
+ } = &mut last_prediction.state
+ else {
+ return;
+ };
+
+ *feedback_state = Some(kind);
+ let text = feedback_editor.update(cx, |feedback_editor, cx| {
+ feedback_editor.set_placeholder_text(
+ "Submitted. Edit or submit again to change.",
+ window,
+ cx,
+ );
+ feedback_editor.text(cx)
+ });
+ cx.notify();
+
+ cx.defer_in(window, {
+ let model_response_editor = model_response_editor.downgrade();
+ move |_, window, cx| {
+ if let Some(model_response_editor) = model_response_editor.upgrade() {
+ model_response_editor.focus_handle(cx).focus(window);
+ }
+ }
+ });
+
+ let kind = match kind {
+ Feedback::Positive => "positive",
+ Feedback::Negative => "negative",
+ };
+
+ telemetry::event!(
+ "Zeta2 Prediction Rated",
+ id = response.request_id,
+ kind = kind,
+ text = text,
+ request = last_prediction.request,
+ response = response,
+ project_snapshot = project_snapshot,
+ );
+ })
+ .log_err();
+ })
+ .detach();
+ }
+
+ fn focus_feedback(&mut self, window: &mut Window, cx: &mut Context<Self>) {
+ if let Some(last_prediction) = self.last_prediction.as_mut() {
+ if let LastPredictionState::Success {
+ feedback_editor, ..
+ } = &mut last_prediction.state
+ {
+ feedback_editor.focus_handle(cx).focus(window);
+ }
+ };
+ }
+
fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
v_flex()
.gap_2()
@@ -618,8 +797,9 @@ impl Zeta2Inspector {
),
ui::ToggleButtonSimple::new(
"Inference",
- cx.listener(|this, _, _, cx| {
+ cx.listener(|this, _, window, cx| {
this.active_view = ActiveView::Inference;
+ this.focus_feedback(window, cx);
cx.notify();
}),
),
@@ -640,21 +820,24 @@ impl Zeta2Inspector {
return None;
};
- let (prompt_planning_time, inference_time, parsing_time) = match &prediction.state {
- LastPredictionState::Success {
- inference_time,
- parsing_time,
- prompt_planning_time,
+ let (prompt_planning_time, inference_time, parsing_time) =
+ if let LastPredictionState::Success {
+ response:
+ PredictEditsResponse {
+ debug_info: Some(debug_info),
+ ..
+ },
..
- } => (
- Some(*prompt_planning_time),
- Some(*inference_time),
- Some(*parsing_time),
- ),
- LastPredictionState::Requested | LastPredictionState::Failed { .. } => {
+ } = &prediction.state
+ {
+ (
+ Some(debug_info.prompt_planning_time),
+ Some(debug_info.inference_time),
+ Some(debug_info.parsing_time),
+ )
+ } else {
(None, None, None)
- }
- };
+ };
Some(
v_flex()
@@ -690,14 +873,16 @@ impl Zeta2Inspector {
})
}
- fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
+ fn render_content(&self, window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
if !cx.has_flag::<Zeta2FeatureFlag>() {
return Self::render_message("`zeta2` feature flag is not enabled");
}
match self.last_prediction.as_ref() {
None => Self::render_message("No prediction"),
- Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
+ Some(prediction) => self
+ .render_last_prediction(prediction, window, cx)
+ .into_any(),
}
}
@@ -710,7 +895,12 @@ impl Zeta2Inspector {
.into_any()
}
- fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
+ fn render_last_prediction(
+ &self,
+ prediction: &LastPrediction,
+ window: &mut Window,
+ cx: &mut Context<Self>,
+ ) -> Div {
match &self.active_view {
ActiveView::Context => div().size_full().child(prediction.context_editor.clone()),
ActiveView::Inference => h_flex()
@@ -748,24 +938,107 @@ impl Zeta2Inspector {
.flex_1()
.gap_2()
.h_full()
- .p_4()
- .child(ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall))
- .child(match &prediction.state {
- LastPredictionState::Success {
- model_response_editor,
- ..
- } => model_response_editor.clone().into_any_element(),
- LastPredictionState::Requested => v_flex()
- .p_4()
+ .child(
+ v_flex()
+ .flex_1()
.gap_2()
- .child(Label::new("Loading...").buffer_font(cx))
- .into_any(),
- LastPredictionState::Failed { message } => v_flex()
.p_4()
- .gap_2()
- .child(Label::new(message.clone()).buffer_font(cx))
- .into_any(),
- }),
+ .child(
+ ui::Headline::new("Model Response")
+ .size(ui::HeadlineSize::XSmall),
+ )
+ .child(match &prediction.state {
+ LastPredictionState::Success {
+ model_response_editor,
+ ..
+ } => model_response_editor.clone().into_any_element(),
+ LastPredictionState::Requested => v_flex()
+ .gap_2()
+ .child(Label::new("Loading...").buffer_font(cx))
+ .into_any_element(),
+ LastPredictionState::Failed { message } => v_flex()
+ .gap_2()
+ .max_w_96()
+ .child(Label::new(message.clone()).buffer_font(cx))
+ .into_any_element(),
+ }),
+ )
+ .child(ui::divider())
+ .child(
+ if prediction.request.can_collect_data
+ && let LastPredictionState::Success {
+ feedback_editor,
+ feedback: feedback_state,
+ ..
+ } = &prediction.state
+ {
+ v_flex()
+ .key_context("Zeta2Feedback")
+ .on_action(cx.listener(Self::handle_rate_positive))
+ .on_action(cx.listener(Self::handle_rate_negative))
+ .gap_2()
+ .p_2()
+ .child(feedback_editor.clone())
+ .child(
+ h_flex()
+ .justify_end()
+ .w_full()
+ .child(
+ ButtonLike::new("rate-positive")
+ .when(
+ *feedback_state == Some(Feedback::Positive),
+ |this| this.style(ButtonStyle::Filled),
+ )
+ .children(
+ KeyBinding::for_action(
+ &Zeta2RatePredictionPositive,
+ window,
+ cx,
+ )
+ .map(|k| k.size(TextSize::Small.rems(cx))),
+ )
+ .child(ui::Icon::new(ui::IconName::ThumbsUp))
+ .on_click(cx.listener(
+ |this, _, window, cx| {
+ this.handle_rate_positive(
+ &Zeta2RatePredictionPositive,
+ window,
+ cx,
+ );
+ },
+ )),
+ )
+ .child(
+ ButtonLike::new("rate-negative")
+ .when(
+ *feedback_state == Some(Feedback::Negative),
+ |this| this.style(ButtonStyle::Filled),
+ )
+ .children(
+ KeyBinding::for_action(
+ &Zeta2RatePredictionNegative,
+ window,
+ cx,
+ )
+ .map(|k| k.size(TextSize::Small.rems(cx))),
+ )
+ .child(ui::Icon::new(ui::IconName::ThumbsDown))
+ .on_click(cx.listener(
+ |this, _, window, cx| {
+ this.handle_rate_negative(
+ &Zeta2RatePredictionNegative,
+ window,
+ cx,
+ );
+ },
+ )),
+ ),
+ )
+ .into_any()
+ } else {
+ Empty.into_any_element()
+ },
+ ),
),
}
}
@@ -808,7 +1081,7 @@ impl Render for Zeta2Inspector {
.child(ui::vertical_divider())
.children(self.render_stats()),
)
- .child(self.render_content(cx))
+ .child(self.render_content(window, cx))
}
}