Release Notes: - N/A --------- Co-authored-by: Agus Zubiaga <agus@zed.dev>
1333 lines
56 KiB
Rust
1333 lines
56 KiB
Rust
mod zeta2_context_view;
|
|
|
|
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::{
|
|
self, DeclarationScoreComponents, PredictEditsRequest, PredictEditsResponse, PromptFormat,
|
|
};
|
|
use collections::HashMap;
|
|
use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
|
|
use feature_flags::FeatureFlagAppExt as _;
|
|
use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared};
|
|
use gpui::{
|
|
CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
|
|
WeakEntity, actions, prelude::*,
|
|
};
|
|
use language::{Buffer, DiskState};
|
|
use ordered_float::OrderedFloat;
|
|
use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot};
|
|
use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
|
|
use ui_input::InputField;
|
|
use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
|
|
use workspace::{Item, SplitDirection, Workspace};
|
|
use zeta2::{
|
|
ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, LlmContextOptions, Zeta, Zeta2FeatureFlag,
|
|
ZetaDebugInfo, ZetaEditPredictionDebugInfo, ZetaOptions,
|
|
};
|
|
|
|
use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
|
|
use zeta2_context_view::Zeta2ContextView;
|
|
|
|
actions!(
|
|
dev,
|
|
[
|
|
/// Opens the edit prediction context view.
|
|
OpenZeta2ContextView,
|
|
/// Opens the edit prediction inspector.
|
|
OpenZeta2Inspector,
|
|
/// Rate prediction as positive.
|
|
Zeta2RatePredictionPositive,
|
|
/// Rate prediction as negative.
|
|
Zeta2RatePredictionNegative,
|
|
]
|
|
);
|
|
|
|
pub fn init(cx: &mut App) {
|
|
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
|
|
workspace.register_action(move |workspace, _: &OpenZeta2Inspector, window, cx| {
|
|
let project = workspace.project();
|
|
workspace.split_item(
|
|
SplitDirection::Right,
|
|
Box::new(cx.new(|cx| {
|
|
Zeta2Inspector::new(
|
|
&project,
|
|
workspace.client(),
|
|
workspace.user_store(),
|
|
window,
|
|
cx,
|
|
)
|
|
})),
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
})
|
|
.detach();
|
|
|
|
cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
|
|
workspace.register_action(move |workspace, _: &OpenZeta2ContextView, window, cx| {
|
|
let project = workspace.project();
|
|
workspace.split_item(
|
|
SplitDirection::Right,
|
|
Box::new(cx.new(|cx| {
|
|
Zeta2ContextView::new(
|
|
project.clone(),
|
|
workspace.client(),
|
|
workspace.user_store(),
|
|
window,
|
|
cx,
|
|
)
|
|
})),
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
})
|
|
.detach();
|
|
}
|
|
|
|
// TODO show included diagnostics, and events
|
|
|
|
pub struct Zeta2Inspector {
|
|
focus_handle: FocusHandle,
|
|
project: Entity<Project>,
|
|
last_prediction: Option<LastPrediction>,
|
|
max_excerpt_bytes_input: Entity<InputField>,
|
|
min_excerpt_bytes_input: Entity<InputField>,
|
|
cursor_context_ratio_input: Entity<InputField>,
|
|
max_prompt_bytes_input: Entity<InputField>,
|
|
context_mode: ContextModeState,
|
|
active_view: ActiveView,
|
|
zeta: Entity<Zeta>,
|
|
_active_editor_subscription: Option<Subscription>,
|
|
_update_state_task: Task<()>,
|
|
_receive_task: Task<()>,
|
|
}
|
|
|
|
pub enum ContextModeState {
|
|
Llm,
|
|
Syntax {
|
|
max_retrieved_declarations: Entity<InputField>,
|
|
},
|
|
}
|
|
|
|
#[derive(PartialEq)]
|
|
enum ActiveView {
|
|
Context,
|
|
Inference,
|
|
}
|
|
|
|
struct LastPrediction {
|
|
context_editor: Entity<Editor>,
|
|
prompt_editor: Entity<Editor>,
|
|
retrieval_time: TimeDelta,
|
|
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 {
|
|
model_response_editor: Entity<Editor>,
|
|
feedback_editor: Entity<Editor>,
|
|
feedback: Option<Feedback>,
|
|
response: predict_edits_v3::PredictEditsResponse,
|
|
},
|
|
Failed {
|
|
message: String,
|
|
},
|
|
}
|
|
|
|
impl Zeta2Inspector {
|
|
pub fn new(
|
|
project: &Entity<Project>,
|
|
client: &Arc<Client>,
|
|
user_store: &Entity<UserStore>,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Self {
|
|
let zeta = Zeta::global(client, user_store, cx);
|
|
let mut request_rx = zeta.update(cx, |zeta, _cx| zeta.debug_info());
|
|
|
|
let receive_task = cx.spawn_in(window, async move |this, cx| {
|
|
while let Some(prediction) = request_rx.next().await {
|
|
this.update_in(cx, |this, window, cx| {
|
|
this.update_last_prediction(prediction, window, cx)
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
|
|
let mut this = Self {
|
|
focus_handle: cx.focus_handle(),
|
|
project: project.clone(),
|
|
last_prediction: None,
|
|
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),
|
|
max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx),
|
|
context_mode: ContextModeState::Llm,
|
|
zeta: zeta.clone(),
|
|
_active_editor_subscription: None,
|
|
_update_state_task: Task::ready(()),
|
|
_receive_task: receive_task,
|
|
};
|
|
this.set_options_state(&zeta.read(cx).options().clone(), window, cx);
|
|
this
|
|
}
|
|
|
|
fn set_options_state(
|
|
&mut self,
|
|
options: &ZetaOptions,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let excerpt_options = options.context.excerpt();
|
|
self.max_excerpt_bytes_input.update(cx, |input, cx| {
|
|
input.set_text(excerpt_options.max_bytes.to_string(), window, cx);
|
|
});
|
|
self.min_excerpt_bytes_input.update(cx, |input, cx| {
|
|
input.set_text(excerpt_options.min_bytes.to_string(), window, cx);
|
|
});
|
|
self.cursor_context_ratio_input.update(cx, |input, cx| {
|
|
input.set_text(
|
|
format!(
|
|
"{:.2}",
|
|
excerpt_options.target_before_cursor_over_total_bytes
|
|
),
|
|
window,
|
|
cx,
|
|
);
|
|
});
|
|
self.max_prompt_bytes_input.update(cx, |input, cx| {
|
|
input.set_text(options.max_prompt_bytes.to_string(), window, cx);
|
|
});
|
|
|
|
match &options.context {
|
|
ContextMode::Llm(_) => {
|
|
self.context_mode = ContextModeState::Llm;
|
|
}
|
|
ContextMode::Syntax(_) => {
|
|
self.context_mode = ContextModeState::Syntax {
|
|
max_retrieved_declarations: Self::number_input(
|
|
"Max Retrieved Definitions",
|
|
window,
|
|
cx,
|
|
),
|
|
};
|
|
}
|
|
}
|
|
cx.notify();
|
|
}
|
|
|
|
fn set_zeta_options(&mut self, options: ZetaOptions, cx: &mut Context<Self>) {
|
|
self.zeta.update(cx, |this, _cx| this.set_options(options));
|
|
|
|
const DEBOUNCE_TIME: Duration = Duration::from_millis(100);
|
|
|
|
if let Some(prediction) = self.last_prediction.as_mut() {
|
|
if let Some(buffer) = prediction.buffer.upgrade() {
|
|
let position = prediction.position;
|
|
let zeta = self.zeta.clone();
|
|
let project = self.project.clone();
|
|
prediction._task = Some(cx.spawn(async move |_this, cx| {
|
|
cx.background_executor().timer(DEBOUNCE_TIME).await;
|
|
if let Some(task) = zeta
|
|
.update(cx, |zeta, cx| {
|
|
zeta.refresh_prediction(&project, &buffer, position, cx)
|
|
})
|
|
.ok()
|
|
{
|
|
task.await.log_err();
|
|
}
|
|
}));
|
|
prediction.state = LastPredictionState::Requested;
|
|
} else {
|
|
self.last_prediction.take();
|
|
}
|
|
}
|
|
|
|
cx.notify();
|
|
}
|
|
|
|
fn number_input(
|
|
label: &'static str,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) -> Entity<InputField> {
|
|
let input = cx.new(|cx| {
|
|
InputField::new(window, cx, "")
|
|
.label(label)
|
|
.label_min_width(px(64.))
|
|
});
|
|
|
|
cx.subscribe_in(
|
|
&input.read(cx).editor().clone(),
|
|
window,
|
|
|this, _, event, _window, cx| {
|
|
let EditorEvent::BufferEdited = event else {
|
|
return;
|
|
};
|
|
|
|
fn number_input_value<T: FromStr + Default>(
|
|
input: &Entity<InputField>,
|
|
cx: &App,
|
|
) -> T {
|
|
input
|
|
.read(cx)
|
|
.editor()
|
|
.read(cx)
|
|
.text(cx)
|
|
.parse::<T>()
|
|
.unwrap_or_default()
|
|
}
|
|
|
|
let zeta_options = this.zeta.read(cx).options().clone();
|
|
|
|
let excerpt_options = EditPredictionExcerptOptions {
|
|
max_bytes: number_input_value(&this.max_excerpt_bytes_input, cx),
|
|
min_bytes: number_input_value(&this.min_excerpt_bytes_input, cx),
|
|
target_before_cursor_over_total_bytes: number_input_value(
|
|
&this.cursor_context_ratio_input,
|
|
cx,
|
|
),
|
|
};
|
|
|
|
let context = match zeta_options.context {
|
|
ContextMode::Llm(_context_options) => ContextMode::Llm(LlmContextOptions {
|
|
excerpt: excerpt_options,
|
|
}),
|
|
ContextMode::Syntax(context_options) => {
|
|
let max_retrieved_declarations = match &this.context_mode {
|
|
ContextModeState::Llm => {
|
|
zeta2::DEFAULT_SYNTAX_CONTEXT_OPTIONS.max_retrieved_declarations
|
|
}
|
|
ContextModeState::Syntax {
|
|
max_retrieved_declarations,
|
|
} => number_input_value(max_retrieved_declarations, cx),
|
|
};
|
|
|
|
ContextMode::Syntax(EditPredictionContextOptions {
|
|
excerpt: excerpt_options,
|
|
max_retrieved_declarations,
|
|
..context_options
|
|
})
|
|
}
|
|
};
|
|
|
|
this.set_zeta_options(
|
|
ZetaOptions {
|
|
context,
|
|
max_prompt_bytes: number_input_value(&this.max_prompt_bytes_input, cx),
|
|
max_diagnostic_bytes: zeta_options.max_diagnostic_bytes,
|
|
prompt_format: zeta_options.prompt_format,
|
|
file_indexing_parallelism: zeta_options.file_indexing_parallelism,
|
|
},
|
|
cx,
|
|
);
|
|
},
|
|
)
|
|
.detach();
|
|
input
|
|
}
|
|
|
|
fn update_last_prediction(
|
|
&mut self,
|
|
prediction: zeta2::ZetaDebugInfo,
|
|
window: &mut Window,
|
|
cx: &mut Context<Self>,
|
|
) {
|
|
let project = self.project.read(cx);
|
|
let path_style = project.path_style(cx);
|
|
let Some(worktree_id) = project
|
|
.worktrees(cx)
|
|
.next()
|
|
.map(|worktree| worktree.read(cx).id())
|
|
else {
|
|
log::error!("Open a worktree to use edit prediction debug view");
|
|
self.last_prediction.take();
|
|
return;
|
|
};
|
|
|
|
self._update_state_task = cx.spawn_in(window, {
|
|
let language_registry = self.project.read(cx).languages().clone();
|
|
async move |this, cx| {
|
|
let mut languages = HashMap::default();
|
|
let ZetaDebugInfo::EditPredicted(prediction) = prediction else {
|
|
return;
|
|
};
|
|
for ext in prediction
|
|
.request
|
|
.referenced_declarations
|
|
.iter()
|
|
.filter_map(|snippet| snippet.path.extension())
|
|
.chain(prediction.request.excerpt_path.extension())
|
|
{
|
|
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
|
|
languages.insert(
|
|
ext.to_owned(),
|
|
language_registry
|
|
.language_for_name_or_extension(&ext.to_string_lossy())
|
|
.await
|
|
.ok(),
|
|
);
|
|
}
|
|
}
|
|
|
|
let markdown_language = language_registry
|
|
.language_for_name("Markdown")
|
|
.await
|
|
.log_err();
|
|
|
|
this.update_in(cx, |this, window, cx| {
|
|
let context_editor = cx.new(|cx| {
|
|
let mut excerpt_score_components = HashMap::default();
|
|
|
|
let multibuffer = cx.new(|cx| {
|
|
let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
|
|
let excerpt_file = Arc::new(ExcerptMetadataFile {
|
|
title: RelPath::unix("Cursor Excerpt").unwrap().into(),
|
|
path_style,
|
|
worktree_id,
|
|
});
|
|
|
|
let excerpt_buffer = cx.new(|cx| {
|
|
let mut buffer =
|
|
Buffer::local(prediction.request.excerpt.clone(), cx);
|
|
if let Some(language) = prediction
|
|
.request
|
|
.excerpt_path
|
|
.extension()
|
|
.and_then(|ext| languages.get(ext))
|
|
{
|
|
buffer.set_language(language.clone(), cx);
|
|
}
|
|
buffer.file_updated(excerpt_file, cx);
|
|
buffer
|
|
});
|
|
|
|
multibuffer.push_excerpts(
|
|
excerpt_buffer,
|
|
[ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
|
|
cx,
|
|
);
|
|
|
|
let mut declarations =
|
|
prediction.request.referenced_declarations.clone();
|
|
declarations.sort_unstable_by_key(|declaration| {
|
|
Reverse(OrderedFloat(declaration.declaration_score))
|
|
});
|
|
|
|
for snippet in &declarations {
|
|
let snippet_file = Arc::new(ExcerptMetadataFile {
|
|
title: RelPath::unix(&format!(
|
|
"{} (Score: {})",
|
|
snippet.path.display(),
|
|
snippet.declaration_score
|
|
))
|
|
.unwrap()
|
|
.into(),
|
|
path_style,
|
|
worktree_id,
|
|
});
|
|
|
|
let excerpt_buffer = cx.new(|cx| {
|
|
let mut buffer = Buffer::local(snippet.text.clone(), cx);
|
|
buffer.file_updated(snippet_file, cx);
|
|
if let Some(ext) = snippet.path.extension()
|
|
&& let Some(language) = languages.get(ext)
|
|
{
|
|
buffer.set_language(language.clone(), cx);
|
|
}
|
|
buffer
|
|
});
|
|
|
|
let excerpt_ids = multibuffer.push_excerpts(
|
|
excerpt_buffer,
|
|
[ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
|
|
cx,
|
|
);
|
|
let excerpt_id = excerpt_ids.first().unwrap();
|
|
|
|
excerpt_score_components
|
|
.insert(*excerpt_id, snippet.score_components.clone());
|
|
}
|
|
|
|
multibuffer
|
|
});
|
|
|
|
let mut editor =
|
|
Editor::new(EditorMode::full(), multibuffer, None, window, cx);
|
|
editor.register_addon(ZetaContextAddon {
|
|
excerpt_score_components,
|
|
});
|
|
editor
|
|
});
|
|
|
|
let ZetaEditPredictionDebugInfo {
|
|
response_rx,
|
|
position,
|
|
buffer,
|
|
retrieval_time,
|
|
local_prompt,
|
|
..
|
|
} = prediction;
|
|
|
|
let task = cx.spawn_in(window, {
|
|
let markdown_language = markdown_language.clone();
|
|
async move |this, cx| {
|
|
let response = response_rx.await;
|
|
|
|
this.update_in(cx, |this, window, cx| {
|
|
if let Some(prediction) = this.last_prediction.as_mut() {
|
|
prediction.state = match response {
|
|
Ok(Ok(response)) => {
|
|
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 {
|
|
model_response_editor: cx.new(|cx| {
|
|
let buffer = cx.new(|cx| {
|
|
let mut buffer = Buffer::local(
|
|
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);
|
|
buffer
|
|
});
|
|
let buffer = cx.new(|cx| {
|
|
MultiBuffer::singleton(buffer, cx)
|
|
});
|
|
let mut editor = Editor::new(
|
|
EditorMode::full(),
|
|
buffer,
|
|
None,
|
|
window,
|
|
cx,
|
|
);
|
|
editor.set_read_only(true);
|
|
editor.set_show_line_numbers(false, cx);
|
|
editor.set_show_gutter(false, cx);
|
|
editor.set_show_scrollbars(false, cx);
|
|
editor
|
|
}),
|
|
feedback_editor,
|
|
feedback: None,
|
|
response,
|
|
}
|
|
}
|
|
Ok(Err(err)) => {
|
|
LastPredictionState::Failed { message: err }
|
|
}
|
|
Err(oneshot::Canceled) => LastPredictionState::Failed {
|
|
message: "Canceled".to_string(),
|
|
},
|
|
};
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
|
|
let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
|
|
|
|
this.last_prediction = Some(LastPrediction {
|
|
context_editor,
|
|
prompt_editor: cx.new(|cx| {
|
|
let buffer = cx.new(|cx| {
|
|
let mut buffer =
|
|
Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
|
|
buffer.set_language(markdown_language.clone(), cx);
|
|
buffer
|
|
});
|
|
let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
|
|
let mut editor =
|
|
Editor::new(EditorMode::full(), buffer, None, window, cx);
|
|
editor.set_read_only(true);
|
|
editor.set_show_line_numbers(false, cx);
|
|
editor.set_show_gutter(false, cx);
|
|
editor.set_show_scrollbars(false, cx);
|
|
editor
|
|
}),
|
|
retrieval_time,
|
|
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();
|
|
})
|
|
.ok();
|
|
}
|
|
});
|
|
}
|
|
|
|
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()
|
|
.child(
|
|
h_flex()
|
|
.child(Headline::new("Options").size(HeadlineSize::Small))
|
|
.justify_between()
|
|
.child(
|
|
ui::Button::new("reset-options", "Reset")
|
|
.disabled(self.zeta.read(cx).options() == &zeta2::DEFAULT_OPTIONS)
|
|
.style(ButtonStyle::Outlined)
|
|
.size(ButtonSize::Large)
|
|
.on_click(cx.listener(|this, _, window, cx| {
|
|
this.set_options_state(&zeta2::DEFAULT_OPTIONS, window, cx);
|
|
})),
|
|
),
|
|
)
|
|
.child(
|
|
v_flex()
|
|
.gap_2()
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.items_end()
|
|
.child(self.max_excerpt_bytes_input.clone())
|
|
.child(self.min_excerpt_bytes_input.clone())
|
|
.child(self.cursor_context_ratio_input.clone())
|
|
.child(self.render_context_mode_dropdown(window, cx)),
|
|
)
|
|
.child(
|
|
h_flex()
|
|
.gap_2()
|
|
.items_end()
|
|
.children(match &self.context_mode {
|
|
ContextModeState::Llm => None,
|
|
ContextModeState::Syntax {
|
|
max_retrieved_declarations,
|
|
} => Some(max_retrieved_declarations.clone()),
|
|
})
|
|
.child(self.max_prompt_bytes_input.clone())
|
|
.child(self.render_prompt_format_dropdown(window, cx)),
|
|
),
|
|
)
|
|
}
|
|
|
|
fn render_context_mode_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
|
let this = cx.weak_entity();
|
|
|
|
v_flex()
|
|
.gap_1p5()
|
|
.child(
|
|
Label::new("Context Mode")
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.child(
|
|
DropdownMenu::new(
|
|
"ep-ctx-mode",
|
|
match &self.context_mode {
|
|
ContextModeState::Llm => "LLM-based",
|
|
ContextModeState::Syntax { .. } => "Syntax",
|
|
},
|
|
ContextMenu::build(window, cx, move |menu, _window, _cx| {
|
|
menu.item(
|
|
ContextMenuEntry::new("LLM-based")
|
|
.toggleable(
|
|
IconPosition::End,
|
|
matches!(self.context_mode, ContextModeState::Llm),
|
|
)
|
|
.handler({
|
|
let this = this.clone();
|
|
move |window, cx| {
|
|
this.update(cx, |this, cx| {
|
|
let current_options =
|
|
this.zeta.read(cx).options().clone();
|
|
match current_options.context.clone() {
|
|
ContextMode::Llm(_) => {}
|
|
ContextMode::Syntax(context_options) => {
|
|
let options = ZetaOptions {
|
|
context: ContextMode::Llm(
|
|
LlmContextOptions {
|
|
excerpt: context_options.excerpt,
|
|
},
|
|
),
|
|
..current_options
|
|
};
|
|
this.set_options_state(&options, window, cx);
|
|
this.set_zeta_options(options, cx);
|
|
}
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
}),
|
|
)
|
|
.item(
|
|
ContextMenuEntry::new("Syntax")
|
|
.toggleable(
|
|
IconPosition::End,
|
|
matches!(self.context_mode, ContextModeState::Syntax { .. }),
|
|
)
|
|
.handler({
|
|
move |window, cx| {
|
|
this.update(cx, |this, cx| {
|
|
let current_options =
|
|
this.zeta.read(cx).options().clone();
|
|
match current_options.context.clone() {
|
|
ContextMode::Llm(context_options) => {
|
|
let options = ZetaOptions {
|
|
context: ContextMode::Syntax(
|
|
EditPredictionContextOptions {
|
|
excerpt: context_options.excerpt,
|
|
..DEFAULT_SYNTAX_CONTEXT_OPTIONS
|
|
},
|
|
),
|
|
..current_options
|
|
};
|
|
this.set_options_state(&options, window, cx);
|
|
this.set_zeta_options(options, cx);
|
|
}
|
|
ContextMode::Syntax(_) => {}
|
|
}
|
|
})
|
|
.ok();
|
|
}
|
|
}),
|
|
)
|
|
}),
|
|
)
|
|
.style(ui::DropdownStyle::Outlined),
|
|
)
|
|
}
|
|
|
|
fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
|
|
let active_format = self.zeta.read(cx).options().prompt_format;
|
|
let this = cx.weak_entity();
|
|
|
|
v_flex()
|
|
.gap_1p5()
|
|
.child(
|
|
Label::new("Prompt Format")
|
|
.size(LabelSize::Small)
|
|
.color(Color::Muted),
|
|
)
|
|
.child(
|
|
DropdownMenu::new(
|
|
"ep-prompt-format",
|
|
active_format.to_string(),
|
|
ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
|
|
for prompt_format in PromptFormat::iter() {
|
|
menu = menu.item(
|
|
ContextMenuEntry::new(prompt_format.to_string())
|
|
.toggleable(IconPosition::End, active_format == prompt_format)
|
|
.handler({
|
|
let this = this.clone();
|
|
move |_window, cx| {
|
|
this.update(cx, |this, cx| {
|
|
let current_options =
|
|
this.zeta.read(cx).options().clone();
|
|
let options = ZetaOptions {
|
|
prompt_format,
|
|
..current_options
|
|
};
|
|
this.set_zeta_options(options, cx);
|
|
})
|
|
.ok();
|
|
}
|
|
}),
|
|
)
|
|
}
|
|
menu
|
|
}),
|
|
)
|
|
.style(ui::DropdownStyle::Outlined),
|
|
)
|
|
}
|
|
|
|
fn render_tabs(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
|
|
if self.last_prediction.is_none() {
|
|
return None;
|
|
};
|
|
|
|
Some(
|
|
ui::ToggleButtonGroup::single_row(
|
|
"prediction",
|
|
[
|
|
ui::ToggleButtonSimple::new(
|
|
"Context",
|
|
cx.listener(|this, _, _, cx| {
|
|
this.active_view = ActiveView::Context;
|
|
cx.notify();
|
|
}),
|
|
),
|
|
ui::ToggleButtonSimple::new(
|
|
"Inference",
|
|
cx.listener(|this, _, window, cx| {
|
|
this.active_view = ActiveView::Inference;
|
|
this.focus_feedback(window, cx);
|
|
cx.notify();
|
|
}),
|
|
),
|
|
],
|
|
)
|
|
.style(ui::ToggleButtonGroupStyle::Outlined)
|
|
.selected_index(if self.active_view == ActiveView::Context {
|
|
0
|
|
} else {
|
|
1
|
|
})
|
|
.into_any_element(),
|
|
)
|
|
}
|
|
|
|
fn render_stats(&self) -> Option<Div> {
|
|
let Some(prediction) = self.last_prediction.as_ref() else {
|
|
return None;
|
|
};
|
|
|
|
let (prompt_planning_time, inference_time, parsing_time) =
|
|
if let LastPredictionState::Success {
|
|
response:
|
|
PredictEditsResponse {
|
|
debug_info: Some(debug_info),
|
|
..
|
|
},
|
|
..
|
|
} = &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()
|
|
.p_4()
|
|
.gap_2()
|
|
.min_w(px(160.))
|
|
.child(Headline::new("Stats").size(HeadlineSize::Small))
|
|
.child(Self::render_duration(
|
|
"Context retrieval",
|
|
Some(prediction.retrieval_time),
|
|
))
|
|
.child(Self::render_duration(
|
|
"Prompt planning",
|
|
prompt_planning_time,
|
|
))
|
|
.child(Self::render_duration("Inference", inference_time))
|
|
.child(Self::render_duration("Parsing", parsing_time)),
|
|
)
|
|
}
|
|
|
|
fn render_duration(name: &'static str, time: Option<chrono::TimeDelta>) -> Div {
|
|
h_flex()
|
|
.gap_1()
|
|
.child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
|
|
.child(match time {
|
|
Some(time) => Label::new(if time.num_microseconds().unwrap_or(0) >= 1000 {
|
|
format!("{} ms", time.num_milliseconds())
|
|
} else {
|
|
format!("{} µs", time.num_microseconds().unwrap_or(0))
|
|
})
|
|
.size(LabelSize::Small),
|
|
None => Label::new("...").size(LabelSize::Small),
|
|
})
|
|
}
|
|
|
|
fn render_content(&self, _: &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(),
|
|
}
|
|
}
|
|
|
|
fn render_message(message: impl Into<SharedString>) -> AnyElement {
|
|
v_flex()
|
|
.size_full()
|
|
.justify_center()
|
|
.items_center()
|
|
.child(Label::new(message).size(LabelSize::Large))
|
|
.into_any()
|
|
}
|
|
|
|
fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
|
|
match &self.active_view {
|
|
ActiveView::Context => div().size_full().child(prediction.context_editor.clone()),
|
|
ActiveView::Inference => h_flex()
|
|
.items_start()
|
|
.w_full()
|
|
.flex_1()
|
|
.border_t_1()
|
|
.border_color(cx.theme().colors().border)
|
|
.bg(cx.theme().colors().editor_background)
|
|
.child(
|
|
v_flex()
|
|
.flex_1()
|
|
.gap_2()
|
|
.p_4()
|
|
.h_full()
|
|
.child(
|
|
h_flex()
|
|
.justify_between()
|
|
.child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
|
|
.child(match prediction.state {
|
|
LastPredictionState::Requested
|
|
| LastPredictionState::Failed { .. } => ui::Chip::new("Local")
|
|
.bg_color(cx.theme().status().warning_background)
|
|
.label_color(Color::Success),
|
|
LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
|
|
.bg_color(cx.theme().status().success_background)
|
|
.label_color(Color::Success),
|
|
}),
|
|
)
|
|
.child(prediction.prompt_editor.clone()),
|
|
)
|
|
.child(ui::vertical_divider())
|
|
.child(
|
|
v_flex()
|
|
.flex_1()
|
|
.gap_2()
|
|
.h_full()
|
|
.child(
|
|
v_flex()
|
|
.flex_1()
|
|
.gap_2()
|
|
.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()
|
|
.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),
|
|
)
|
|
.child(
|
|
KeyBinding::for_action(
|
|
&Zeta2RatePredictionPositive,
|
|
cx,
|
|
)
|
|
.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),
|
|
)
|
|
.child(
|
|
KeyBinding::for_action(
|
|
&Zeta2RatePredictionNegative,
|
|
cx,
|
|
)
|
|
.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()
|
|
},
|
|
),
|
|
),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Focusable for Zeta2Inspector {
|
|
fn focus_handle(&self, _cx: &App) -> FocusHandle {
|
|
self.focus_handle.clone()
|
|
}
|
|
}
|
|
|
|
impl Item for Zeta2Inspector {
|
|
type Event = ();
|
|
|
|
fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
|
|
"Zeta2 Inspector".into()
|
|
}
|
|
}
|
|
|
|
impl EventEmitter<()> for Zeta2Inspector {}
|
|
|
|
impl Render for Zeta2Inspector {
|
|
fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
v_flex()
|
|
.size_full()
|
|
.bg(cx.theme().colors().editor_background)
|
|
.child(
|
|
h_flex()
|
|
.w_full()
|
|
.child(
|
|
v_flex()
|
|
.flex_1()
|
|
.p_4()
|
|
.h_full()
|
|
.justify_between()
|
|
.child(self.render_options(window, cx))
|
|
.gap_4()
|
|
.children(self.render_tabs(cx)),
|
|
)
|
|
.child(ui::vertical_divider())
|
|
.children(self.render_stats()),
|
|
)
|
|
.child(self.render_content(window, cx))
|
|
}
|
|
}
|
|
|
|
// Using same approach as commit view
|
|
|
|
struct ExcerptMetadataFile {
|
|
title: Arc<RelPath>,
|
|
worktree_id: WorktreeId,
|
|
path_style: PathStyle,
|
|
}
|
|
|
|
impl language::File for ExcerptMetadataFile {
|
|
fn as_local(&self) -> Option<&dyn language::LocalFile> {
|
|
None
|
|
}
|
|
|
|
fn disk_state(&self) -> DiskState {
|
|
DiskState::New
|
|
}
|
|
|
|
fn path(&self) -> &Arc<RelPath> {
|
|
&self.title
|
|
}
|
|
|
|
fn full_path(&self, _: &App) -> PathBuf {
|
|
self.title.as_std_path().to_path_buf()
|
|
}
|
|
|
|
fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
|
|
self.title.file_name().unwrap()
|
|
}
|
|
|
|
fn path_style(&self, _: &App) -> PathStyle {
|
|
self.path_style
|
|
}
|
|
|
|
fn worktree_id(&self, _: &App) -> WorktreeId {
|
|
self.worktree_id
|
|
}
|
|
|
|
fn to_proto(&self, _: &App) -> language::proto::File {
|
|
unimplemented!()
|
|
}
|
|
|
|
fn is_private(&self) -> bool {
|
|
false
|
|
}
|
|
}
|
|
|
|
struct ZetaContextAddon {
|
|
excerpt_score_components: HashMap<editor::ExcerptId, DeclarationScoreComponents>,
|
|
}
|
|
|
|
impl editor::Addon for ZetaContextAddon {
|
|
fn to_any(&self) -> &dyn std::any::Any {
|
|
self
|
|
}
|
|
|
|
fn render_buffer_header_controls(
|
|
&self,
|
|
excerpt_info: &multi_buffer::ExcerptInfo,
|
|
_window: &Window,
|
|
_cx: &App,
|
|
) -> Option<AnyElement> {
|
|
let score_components = self.excerpt_score_components.get(&excerpt_info.id)?.clone();
|
|
|
|
Some(
|
|
div()
|
|
.id(excerpt_info.id.to_proto() as usize)
|
|
.child(ui::Icon::new(IconName::Info))
|
|
.cursor(CursorStyle::PointingHand)
|
|
.tooltip(move |_, cx| {
|
|
cx.new(|_| ScoreComponentsTooltip::new(&score_components))
|
|
.into()
|
|
})
|
|
.into_any(),
|
|
)
|
|
}
|
|
}
|
|
|
|
struct ScoreComponentsTooltip {
|
|
text: SharedString,
|
|
}
|
|
|
|
impl ScoreComponentsTooltip {
|
|
fn new(components: &DeclarationScoreComponents) -> Self {
|
|
Self {
|
|
text: format!("{:#?}", components).into(),
|
|
}
|
|
}
|
|
}
|
|
|
|
impl Render for ScoreComponentsTooltip {
|
|
fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
|
|
div().pl_2().pt_2p5().child(
|
|
div()
|
|
.elevation_2(cx)
|
|
.py_1()
|
|
.px_2()
|
|
.child(ui::Label::new(self.text.clone()).buffer_font(cx)),
|
|
)
|
|
}
|
|
}
|