diff --git a/Cargo.lock b/Cargo.lock index 5441a1704061e11d0819997ee0ccd7b04b16fac9..8d78c1809b52ff20bf059a619efa98c053abe178 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -20641,6 +20641,7 @@ dependencies = [ "chrono", "clap", "client", + "cloud_llm_client", "collections", "edit_prediction_context", "editor", diff --git a/crates/cloud_llm_client/src/predict_edits_v3.rs b/crates/cloud_llm_client/src/predict_edits_v3.rs index 21f32d674443282b1793e27609f6b29221d7e966..fd924de4406497ab8ab8f7c520a63b28cced98d6 100644 --- a/crates/cloud_llm_client/src/predict_edits_v3.rs +++ b/crates/cloud_llm_client/src/predict_edits_v3.rs @@ -5,6 +5,7 @@ use std::{ path::{Path, PathBuf}, sync::Arc, }; +use strum::EnumIter; use uuid::Uuid; use crate::PredictEditsGitInfo; @@ -42,13 +43,28 @@ pub struct PredictEditsRequest { pub prompt_format: PromptFormat, } -#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] +#[derive(Default, Debug, Clone, Copy, Serialize, Deserialize, PartialEq, EnumIter)] pub enum PromptFormat { #[default] MarkedExcerpt, LabeledSections, } +impl PromptFormat { + pub fn iter() -> impl Iterator { + ::iter() + } +} + +impl std::fmt::Display for PromptFormat { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + PromptFormat::MarkedExcerpt => write!(f, "Marked Excerpt"), + PromptFormat::LabeledSections => write!(f, "Labeled Sections"), + } + } +} + #[derive(Debug, Clone, Serialize, Deserialize)] #[cfg_attr(any(test, feature = "test-support"), derive(PartialEq))] #[serde(tag = "event")] diff --git a/crates/zeta2_tools/Cargo.toml b/crates/zeta2_tools/Cargo.toml index fe651c14193db41f24af00d71835ffd4c6deb6eb..e2dd18e46efee5faa89918c6b11efa8bf5b5422b 100644 --- a/crates/zeta2_tools/Cargo.toml +++ b/crates/zeta2_tools/Cargo.toml @@ -14,6 +14,7 @@ path = "src/zeta2_tools.rs" [dependencies] chrono.workspace = true client.workspace = true +cloud_llm_client.workspace = true collections.workspace = true edit_prediction_context.workspace = true editor.workspace = true diff --git a/crates/zeta2_tools/src/zeta2_tools.rs b/crates/zeta2_tools/src/zeta2_tools.rs index 3913677915b3535b1e2be3993606457033edbf33..3ce5f4359438f483a30749efe083f0599cd090b5 100644 --- a/crates/zeta2_tools/src/zeta2_tools.rs +++ b/crates/zeta2_tools/src/zeta2_tools.rs @@ -2,6 +2,7 @@ use std::{collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc, use chrono::TimeDelta; use client::{Client, UserStore}; +use cloud_llm_client::predict_edits_v3::PromptFormat; use collections::HashMap; use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer}; use futures::StreamExt as _; @@ -11,7 +12,7 @@ use gpui::{ }; use language::{Buffer, DiskState}; use project::{Project, WorktreeId}; -use ui::prelude::*; +use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*}; use ui_input::SingleLineInput; use util::{ResultExt, paths::PathStyle, rel_path::RelPath}; use workspace::{Item, SplitDirection, Workspace}; @@ -425,12 +426,162 @@ impl Zeta2Inspector { }); } + fn render_options(&self, window: &mut Window, cx: &mut Context) -> 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_input_options(&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( + h_flex() + .gap_2() + .items_end() + .child(self.max_prompt_bytes_input.clone()) + .child(self.render_prompt_format_dropdown(window, cx)), + ), + ) + } + + fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context) -> 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_options(options, cx); + }) + .ok(); + } + }), + ) + } + menu + }), + ) + .style(ui::DropdownStyle::Outlined), + ) + } + + fn render_tabs(&self, cx: &mut Context) -> Option { + let Some(LastPredictionState::Success { .. } | LastPredictionState::Replaying { .. }) = + self.last_prediction.as_ref() + else { + 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, _, _, cx| { + this.active_view = ActiveView::Inference; + 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
{ + let Some( + LastPredictionState::Success(prediction) + | LastPredictionState::Replaying { prediction, .. }, + ) = self.last_prediction.as_ref() + else { + return 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", + prediction.retrieval_time, + )) + .child(Self::render_duration( + "Prompt planning", + prediction.prompt_planning_time, + )) + .child(Self::render_duration( + "Inference", + prediction.inference_time, + )) + .child(Self::render_duration("Parsing", prediction.parsing_time)), + ) + } + fn render_duration(name: &'static str, time: chrono::TimeDelta) -> Div { h_flex() .gap_1() .child(Label::new(name).color(Color::Muted).size(LabelSize::Small)) .child( - Label::new(if time.num_microseconds().unwrap_or(0) > 1000 { + Label::new(if time.num_microseconds().unwrap_or(0) >= 1000 { format!("{} ms", time.num_milliseconds()) } else { format!("{} µs", time.num_microseconds().unwrap_or(0)) @@ -439,6 +590,29 @@ impl Zeta2Inspector { ) } + fn render_content(&self, cx: &mut Context) -> AnyElement { + match self.last_prediction.as_ref() { + None => v_flex() + .size_full() + .justify_center() + .items_center() + .child(Label::new("No prediction").size(LabelSize::Large)) + .into_any(), + Some(LastPredictionState::Success(prediction)) => { + self.render_last_prediction(prediction, cx).into_any() + } + Some(LastPredictionState::Replaying { prediction, _task }) => self + .render_last_prediction(prediction, cx) + .opacity(0.6) + .into_any(), + Some(LastPredictionState::Failed(err)) => v_flex() + .p_4() + .gap_2() + .child(Label::new(err.clone()).buffer_font(cx)) + .into_any(), + } + } + fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context) -> Div { match &self.active_view { ActiveView::Context => div().size_full().child(prediction.context_editor.clone()), @@ -489,28 +663,7 @@ impl Item for Zeta2Inspector { impl EventEmitter<()> for Zeta2Inspector {} impl Render for Zeta2Inspector { - fn render(&mut self, _window: &mut Window, cx: &mut Context) -> impl IntoElement { - let content = match self.last_prediction.as_ref() { - None => v_flex() - .size_full() - .justify_center() - .items_center() - .child(Label::new("No prediction").size(LabelSize::Large)) - .into_any(), - Some(LastPredictionState::Success(prediction)) => { - self.render_last_prediction(prediction, cx).into_any() - } - Some(LastPredictionState::Replaying { prediction, _task }) => self - .render_last_prediction(prediction, cx) - .opacity(0.6) - .into_any(), - Some(LastPredictionState::Failed(err)) => v_flex() - .p_4() - .gap_2() - .child(Label::new(err.clone()).buffer_font(cx)) - .into_any(), - }; - + fn render(&mut self, window: &mut Window, cx: &mut Context) -> impl IntoElement { v_flex() .size_full() .bg(cx.theme().colors().editor_background) @@ -523,114 +676,14 @@ impl Render for Zeta2Inspector { .p_4() .h_full() .justify_between() - .child( - v_flex() - .gap_2() - .child(Headline::new("Options").size(HeadlineSize::Small)) - .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.max_prompt_bytes_input.clone()) - .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_input_options( - &zeta2::DEFAULT_OPTIONS, - window, - cx, - ); - }, - )), - ), - ), - ) - .map(|this| { - if let Some( - LastPredictionState::Success { .. } - | LastPredictionState::Replaying { .. }, - ) = self.last_prediction.as_ref() - { - this.child( - 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, _, _, cx| { - this.active_view = ActiveView::Inference; - cx.notify(); - }), - ), - ], - ) - .style(ui::ToggleButtonGroupStyle::Outlined) - .selected_index( - if self.active_view == ActiveView::Context { - 0 - } else { - 1 - }, - ), - ) - } else { - this - } - }), + .child(self.render_options(window, cx)) + .gap_4() + .children(self.render_tabs(cx)), ) .child(ui::vertical_divider()) - .map(|this| { - if let Some( - LastPredictionState::Success(prediction) - | LastPredictionState::Replaying { prediction, .. }, - ) = self.last_prediction.as_ref() - { - this.child( - v_flex() - .p_4() - .gap_2() - .min_w(px(160.)) - .child(Headline::new("Stats").size(HeadlineSize::Small)) - .child(Self::render_duration( - "Context retrieval", - prediction.retrieval_time, - )) - .child(Self::render_duration( - "Prompt planning", - prediction.prompt_planning_time, - )) - .child(Self::render_duration( - "Inference", - prediction.inference_time, - )) - .child(Self::render_duration( - "Parsing", - prediction.parsing_time, - )), - ) - } else { - this - } - }), + .children(self.render_stats()), ) - .child(content) + .child(self.render_content(cx)) } }