zeta2_tools.rs

  1use std::{collections::hash_map::Entry, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
  2
  3use chrono::TimeDelta;
  4use client::{Client, UserStore};
  5use cloud_llm_client::predict_edits_v3::PromptFormat;
  6use collections::HashMap;
  7use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
  8use futures::StreamExt as _;
  9use gpui::{
 10    Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, actions,
 11    prelude::*,
 12};
 13use language::{Buffer, DiskState};
 14use project::{Project, WorktreeId};
 15use ui::{ContextMenu, ContextMenuEntry, DropdownMenu, prelude::*};
 16use ui_input::SingleLineInput;
 17use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
 18use workspace::{Item, SplitDirection, Workspace};
 19use zeta2::{DEFAULT_CONTEXT_OPTIONS, Zeta, ZetaOptions};
 20
 21use edit_prediction_context::{DeclarationStyle, EditPredictionExcerptOptions};
 22
 23actions!(
 24    dev,
 25    [
 26        /// Opens the language server protocol logs viewer.
 27        OpenZeta2Inspector
 28    ]
 29);
 30
 31pub fn init(cx: &mut App) {
 32    cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
 33        workspace.register_action(move |workspace, _: &OpenZeta2Inspector, window, cx| {
 34            let project = workspace.project();
 35            workspace.split_item(
 36                SplitDirection::Right,
 37                Box::new(cx.new(|cx| {
 38                    Zeta2Inspector::new(
 39                        &project,
 40                        workspace.client(),
 41                        workspace.user_store(),
 42                        window,
 43                        cx,
 44                    )
 45                })),
 46                window,
 47                cx,
 48            );
 49        });
 50    })
 51    .detach();
 52}
 53
 54// TODO show included diagnostics, and events
 55
 56pub struct Zeta2Inspector {
 57    focus_handle: FocusHandle,
 58    project: Entity<Project>,
 59    last_prediction: Option<LastPredictionState>,
 60    max_excerpt_bytes_input: Entity<SingleLineInput>,
 61    min_excerpt_bytes_input: Entity<SingleLineInput>,
 62    cursor_context_ratio_input: Entity<SingleLineInput>,
 63    max_prompt_bytes_input: Entity<SingleLineInput>,
 64    active_view: ActiveView,
 65    zeta: Entity<Zeta>,
 66    _active_editor_subscription: Option<Subscription>,
 67    _update_state_task: Task<()>,
 68    _receive_task: Task<()>,
 69}
 70
 71#[derive(PartialEq)]
 72enum ActiveView {
 73    Context,
 74    Inference,
 75}
 76
 77enum LastPredictionState {
 78    Failed(SharedString),
 79    Success(LastPrediction),
 80    Replaying {
 81        prediction: LastPrediction,
 82        _task: Task<()>,
 83    },
 84}
 85
 86struct LastPrediction {
 87    context_editor: Entity<Editor>,
 88    retrieval_time: TimeDelta,
 89    prompt_planning_time: TimeDelta,
 90    inference_time: TimeDelta,
 91    parsing_time: TimeDelta,
 92    prompt_editor: Entity<Editor>,
 93    model_response_editor: Entity<Editor>,
 94    buffer: WeakEntity<Buffer>,
 95    position: language::Anchor,
 96}
 97
 98impl Zeta2Inspector {
 99    pub fn new(
100        project: &Entity<Project>,
101        client: &Arc<Client>,
102        user_store: &Entity<UserStore>,
103        window: &mut Window,
104        cx: &mut Context<Self>,
105    ) -> Self {
106        let zeta = Zeta::global(client, user_store, cx);
107        let mut request_rx = zeta.update(cx, |zeta, _cx| zeta.debug_info());
108
109        let receive_task = cx.spawn_in(window, async move |this, cx| {
110            while let Some(prediction_result) = request_rx.next().await {
111                this.update_in(cx, |this, window, cx| match prediction_result {
112                    Ok(prediction) => {
113                        this.update_last_prediction(prediction, window, cx);
114                    }
115                    Err(err) => {
116                        this.last_prediction = Some(LastPredictionState::Failed(err.into()));
117                        cx.notify();
118                    }
119                })
120                .ok();
121            }
122        });
123
124        let mut this = Self {
125            focus_handle: cx.focus_handle(),
126            project: project.clone(),
127            last_prediction: None,
128            active_view: ActiveView::Context,
129            max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
130            min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
131            cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
132            max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx),
133            zeta: zeta.clone(),
134            _active_editor_subscription: None,
135            _update_state_task: Task::ready(()),
136            _receive_task: receive_task,
137        };
138        this.set_input_options(&zeta.read(cx).options().clone(), window, cx);
139        this
140    }
141
142    fn set_input_options(
143        &mut self,
144        options: &ZetaOptions,
145        window: &mut Window,
146        cx: &mut Context<Self>,
147    ) {
148        self.max_excerpt_bytes_input.update(cx, |input, cx| {
149            input.set_text(options.context.excerpt.max_bytes.to_string(), window, cx);
150        });
151        self.min_excerpt_bytes_input.update(cx, |input, cx| {
152            input.set_text(options.context.excerpt.min_bytes.to_string(), window, cx);
153        });
154        self.cursor_context_ratio_input.update(cx, |input, cx| {
155            input.set_text(
156                format!(
157                    "{:.2}",
158                    options
159                        .context
160                        .excerpt
161                        .target_before_cursor_over_total_bytes
162                ),
163                window,
164                cx,
165            );
166        });
167        self.max_prompt_bytes_input.update(cx, |input, cx| {
168            input.set_text(options.max_prompt_bytes.to_string(), window, cx);
169        });
170        cx.notify();
171    }
172
173    fn set_options(&mut self, options: ZetaOptions, cx: &mut Context<Self>) {
174        self.zeta.update(cx, |this, _cx| this.set_options(options));
175
176        const THROTTLE_TIME: Duration = Duration::from_millis(100);
177
178        if let Some(
179            LastPredictionState::Success(prediction)
180            | LastPredictionState::Replaying { prediction, .. },
181        ) = self.last_prediction.take()
182        {
183            if let Some(buffer) = prediction.buffer.upgrade() {
184                let position = prediction.position;
185                let zeta = self.zeta.clone();
186                let project = self.project.clone();
187                let task = cx.spawn(async move |_this, cx| {
188                    cx.background_executor().timer(THROTTLE_TIME).await;
189                    if let Some(task) = zeta
190                        .update(cx, |zeta, cx| {
191                            zeta.refresh_prediction(&project, &buffer, position, cx)
192                        })
193                        .ok()
194                    {
195                        task.await.log_err();
196                    }
197                });
198                self.last_prediction = Some(LastPredictionState::Replaying {
199                    prediction,
200                    _task: task,
201                });
202            } else {
203                self.last_prediction = Some(LastPredictionState::Failed("Buffer dropped".into()));
204            }
205        }
206
207        cx.notify();
208    }
209
210    fn number_input(
211        label: &'static str,
212        window: &mut Window,
213        cx: &mut Context<Self>,
214    ) -> Entity<SingleLineInput> {
215        let input = cx.new(|cx| {
216            SingleLineInput::new(window, cx, "")
217                .label(label)
218                .label_min_width(px(64.))
219        });
220
221        cx.subscribe_in(
222            &input.read(cx).editor().clone(),
223            window,
224            |this, _, event, _window, cx| {
225                let EditorEvent::BufferEdited = event else {
226                    return;
227                };
228
229                fn number_input_value<T: FromStr + Default>(
230                    input: &Entity<SingleLineInput>,
231                    cx: &App,
232                ) -> T {
233                    input
234                        .read(cx)
235                        .editor()
236                        .read(cx)
237                        .text(cx)
238                        .parse::<T>()
239                        .unwrap_or_default()
240                }
241
242                let mut context_options = DEFAULT_CONTEXT_OPTIONS.clone();
243                context_options.excerpt = EditPredictionExcerptOptions {
244                    max_bytes: number_input_value(&this.max_excerpt_bytes_input, cx),
245                    min_bytes: number_input_value(&this.min_excerpt_bytes_input, cx),
246                    target_before_cursor_over_total_bytes: number_input_value(
247                        &this.cursor_context_ratio_input,
248                        cx,
249                    ),
250                };
251
252                let zeta_options = this.zeta.read(cx).options();
253                this.set_options(
254                    ZetaOptions {
255                        context: context_options,
256                        max_prompt_bytes: number_input_value(&this.max_prompt_bytes_input, cx),
257                        max_diagnostic_bytes: zeta_options.max_diagnostic_bytes,
258                        prompt_format: zeta_options.prompt_format,
259                        file_indexing_parallelism: zeta_options.file_indexing_parallelism,
260                    },
261                    cx,
262                );
263            },
264        )
265        .detach();
266        input
267    }
268
269    fn update_last_prediction(
270        &mut self,
271        prediction: zeta2::PredictionDebugInfo,
272        window: &mut Window,
273        cx: &mut Context<Self>,
274    ) {
275        let project = self.project.read(cx);
276        let path_style = project.path_style(cx);
277        let Some(worktree_id) = project
278            .worktrees(cx)
279            .next()
280            .map(|worktree| worktree.read(cx).id())
281        else {
282            log::error!("Open a worktree to use edit prediction debug view");
283            self.last_prediction.take();
284            return;
285        };
286
287        self._update_state_task = cx.spawn_in(window, {
288            let language_registry = self.project.read(cx).languages().clone();
289            async move |this, cx| {
290                let mut languages = HashMap::default();
291                for lang_id in prediction
292                    .context
293                    .declarations
294                    .iter()
295                    .map(|snippet| snippet.declaration.identifier().language_id)
296                    .chain(prediction.context.excerpt_text.language_id)
297                {
298                    if let Entry::Vacant(entry) = languages.entry(lang_id) {
299                        // Most snippets are gonna be the same language,
300                        // so we think it's fine to do this sequentially for now
301                        entry.insert(language_registry.language_for_id(lang_id).await.ok());
302                    }
303                }
304
305                let markdown_language = language_registry
306                    .language_for_name("Markdown")
307                    .await
308                    .log_err();
309
310                this.update_in(cx, |this, window, cx| {
311                    let context_editor = cx.new(|cx| {
312                        let multibuffer = cx.new(|cx| {
313                            let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
314                            let excerpt_file = Arc::new(ExcerptMetadataFile {
315                                title: RelPath::unix("Cursor Excerpt").unwrap().into(),
316                                path_style,
317                                worktree_id,
318                            });
319
320                            let excerpt_buffer = cx.new(|cx| {
321                                let mut buffer =
322                                    Buffer::local(prediction.context.excerpt_text.body, cx);
323                                if let Some(language) = prediction
324                                    .context
325                                    .excerpt_text
326                                    .language_id
327                                    .as_ref()
328                                    .and_then(|id| languages.get(id))
329                                {
330                                    buffer.set_language(language.clone(), cx);
331                                }
332                                buffer.file_updated(excerpt_file, cx);
333                                buffer
334                            });
335
336                            multibuffer.push_excerpts(
337                                excerpt_buffer,
338                                [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
339                                cx,
340                            );
341
342                            for snippet in &prediction.context.declarations {
343                                let path = this
344                                    .project
345                                    .read(cx)
346                                    .path_for_entry(snippet.declaration.project_entry_id(), cx);
347
348                                let snippet_file = Arc::new(ExcerptMetadataFile {
349                                    title: RelPath::unix(&format!(
350                                        "{} (Score density: {})",
351                                        path.map(|p| p.path.display(path_style).to_string())
352                                            .unwrap_or_else(|| "".to_string()),
353                                        snippet.score_density(DeclarationStyle::Declaration)
354                                    ))
355                                    .unwrap()
356                                    .into(),
357                                    path_style,
358                                    worktree_id,
359                                });
360
361                                let excerpt_buffer = cx.new(|cx| {
362                                    let mut buffer =
363                                        Buffer::local(snippet.declaration.item_text().0, cx);
364                                    buffer.file_updated(snippet_file, cx);
365                                    if let Some(language) =
366                                        languages.get(&snippet.declaration.identifier().language_id)
367                                    {
368                                        buffer.set_language(language.clone(), cx);
369                                    }
370                                    buffer
371                                });
372
373                                multibuffer.push_excerpts(
374                                    excerpt_buffer,
375                                    [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
376                                    cx,
377                                );
378                            }
379
380                            multibuffer
381                        });
382
383                        Editor::new(EditorMode::full(), multibuffer, None, window, cx)
384                    });
385
386                    let last_prediction = LastPrediction {
387                        context_editor,
388                        prompt_editor: cx.new(|cx| {
389                            let buffer = cx.new(|cx| {
390                                let mut buffer = Buffer::local(prediction.request.prompt, cx);
391                                buffer.set_language(markdown_language.clone(), cx);
392                                buffer
393                            });
394                            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
395                            let mut editor =
396                                Editor::new(EditorMode::full(), buffer, None, window, cx);
397                            editor.set_read_only(true);
398                            editor.set_show_line_numbers(false, cx);
399                            editor.set_show_gutter(false, cx);
400                            editor.set_show_scrollbars(false, cx);
401                            editor
402                        }),
403                        model_response_editor: cx.new(|cx| {
404                            let buffer = cx.new(|cx| {
405                                let mut buffer =
406                                    Buffer::local(prediction.request.model_response, cx);
407                                buffer.set_language(markdown_language, cx);
408                                buffer
409                            });
410                            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
411                            let mut editor =
412                                Editor::new(EditorMode::full(), buffer, None, window, cx);
413                            editor.set_read_only(true);
414                            editor.set_show_line_numbers(false, cx);
415                            editor.set_show_gutter(false, cx);
416                            editor.set_show_scrollbars(false, cx);
417                            editor
418                        }),
419                        retrieval_time: prediction.retrieval_time,
420                        prompt_planning_time: prediction.request.prompt_planning_time,
421                        inference_time: prediction.request.inference_time,
422                        parsing_time: prediction.request.parsing_time,
423                        buffer: prediction.buffer,
424                        position: prediction.position,
425                    };
426                    this.last_prediction = Some(LastPredictionState::Success(last_prediction));
427                    cx.notify();
428                })
429                .ok();
430            }
431        });
432    }
433
434    fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
435        v_flex()
436            .gap_2()
437            .child(
438                h_flex()
439                    .child(Headline::new("Options").size(HeadlineSize::Small))
440                    .justify_between()
441                    .child(
442                        ui::Button::new("reset-options", "Reset")
443                            .disabled(self.zeta.read(cx).options() == &zeta2::DEFAULT_OPTIONS)
444                            .style(ButtonStyle::Outlined)
445                            .size(ButtonSize::Large)
446                            .on_click(cx.listener(|this, _, window, cx| {
447                                this.set_input_options(&zeta2::DEFAULT_OPTIONS, window, cx);
448                            })),
449                    ),
450            )
451            .child(
452                v_flex()
453                    .gap_2()
454                    .child(
455                        h_flex()
456                            .gap_2()
457                            .items_end()
458                            .child(self.max_excerpt_bytes_input.clone())
459                            .child(self.min_excerpt_bytes_input.clone())
460                            .child(self.cursor_context_ratio_input.clone()),
461                    )
462                    .child(
463                        h_flex()
464                            .gap_2()
465                            .items_end()
466                            .child(self.max_prompt_bytes_input.clone())
467                            .child(self.render_prompt_format_dropdown(window, cx)),
468                    ),
469            )
470    }
471
472    fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
473        let active_format = self.zeta.read(cx).options().prompt_format;
474        let this = cx.weak_entity();
475
476        v_flex()
477            .gap_1p5()
478            .child(
479                Label::new("Prompt Format")
480                    .size(LabelSize::Small)
481                    .color(Color::Muted),
482            )
483            .child(
484                DropdownMenu::new(
485                    "ep-prompt-format",
486                    active_format.to_string(),
487                    ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
488                        for prompt_format in PromptFormat::iter() {
489                            menu = menu.item(
490                                ContextMenuEntry::new(prompt_format.to_string())
491                                    .toggleable(IconPosition::End, active_format == prompt_format)
492                                    .handler({
493                                        let this = this.clone();
494                                        move |_window, cx| {
495                                            this.update(cx, |this, cx| {
496                                                let current_options =
497                                                    this.zeta.read(cx).options().clone();
498                                                let options = ZetaOptions {
499                                                    prompt_format,
500                                                    ..current_options
501                                                };
502                                                this.set_options(options, cx);
503                                            })
504                                            .ok();
505                                        }
506                                    }),
507                            )
508                        }
509                        menu
510                    }),
511                )
512                .style(ui::DropdownStyle::Outlined),
513            )
514    }
515
516    fn render_tabs(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
517        let Some(LastPredictionState::Success { .. } | LastPredictionState::Replaying { .. }) =
518            self.last_prediction.as_ref()
519        else {
520            return None;
521        };
522
523        Some(
524            ui::ToggleButtonGroup::single_row(
525                "prediction",
526                [
527                    ui::ToggleButtonSimple::new(
528                        "Context",
529                        cx.listener(|this, _, _, cx| {
530                            this.active_view = ActiveView::Context;
531                            cx.notify();
532                        }),
533                    ),
534                    ui::ToggleButtonSimple::new(
535                        "Inference",
536                        cx.listener(|this, _, _, cx| {
537                            this.active_view = ActiveView::Inference;
538                            cx.notify();
539                        }),
540                    ),
541                ],
542            )
543            .style(ui::ToggleButtonGroupStyle::Outlined)
544            .selected_index(if self.active_view == ActiveView::Context {
545                0
546            } else {
547                1
548            })
549            .into_any_element(),
550        )
551    }
552
553    fn render_stats(&self) -> Option<Div> {
554        let Some(
555            LastPredictionState::Success(prediction)
556            | LastPredictionState::Replaying { prediction, .. },
557        ) = self.last_prediction.as_ref()
558        else {
559            return None;
560        };
561
562        Some(
563            v_flex()
564                .p_4()
565                .gap_2()
566                .min_w(px(160.))
567                .child(Headline::new("Stats").size(HeadlineSize::Small))
568                .child(Self::render_duration(
569                    "Context retrieval",
570                    prediction.retrieval_time,
571                ))
572                .child(Self::render_duration(
573                    "Prompt planning",
574                    prediction.prompt_planning_time,
575                ))
576                .child(Self::render_duration(
577                    "Inference",
578                    prediction.inference_time,
579                ))
580                .child(Self::render_duration("Parsing", prediction.parsing_time)),
581        )
582    }
583
584    fn render_duration(name: &'static str, time: chrono::TimeDelta) -> Div {
585        h_flex()
586            .gap_1()
587            .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
588            .child(
589                Label::new(if time.num_microseconds().unwrap_or(0) >= 1000 {
590                    format!("{} ms", time.num_milliseconds())
591                } else {
592                    format!("{} ยตs", time.num_microseconds().unwrap_or(0))
593                })
594                .size(LabelSize::Small),
595            )
596    }
597
598    fn render_content(&self, cx: &mut Context<Self>) -> AnyElement {
599        match self.last_prediction.as_ref() {
600            None => v_flex()
601                .size_full()
602                .justify_center()
603                .items_center()
604                .child(Label::new("No prediction").size(LabelSize::Large))
605                .into_any(),
606            Some(LastPredictionState::Success(prediction)) => {
607                self.render_last_prediction(prediction, cx).into_any()
608            }
609            Some(LastPredictionState::Replaying { prediction, _task }) => self
610                .render_last_prediction(prediction, cx)
611                .opacity(0.6)
612                .into_any(),
613            Some(LastPredictionState::Failed(err)) => v_flex()
614                .p_4()
615                .gap_2()
616                .child(Label::new(err.clone()).buffer_font(cx))
617                .into_any(),
618        }
619    }
620
621    fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
622        match &self.active_view {
623            ActiveView::Context => div().size_full().child(prediction.context_editor.clone()),
624            ActiveView::Inference => h_flex()
625                .items_start()
626                .w_full()
627                .flex_1()
628                .border_t_1()
629                .border_color(cx.theme().colors().border)
630                .bg(cx.theme().colors().editor_background)
631                .child(
632                    v_flex()
633                        .flex_1()
634                        .gap_2()
635                        .p_4()
636                        .h_full()
637                        .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
638                        .child(prediction.prompt_editor.clone()),
639                )
640                .child(ui::vertical_divider())
641                .child(
642                    v_flex()
643                        .flex_1()
644                        .gap_2()
645                        .h_full()
646                        .p_4()
647                        .child(ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall))
648                        .child(prediction.model_response_editor.clone()),
649                ),
650        }
651    }
652}
653
654impl Focusable for Zeta2Inspector {
655    fn focus_handle(&self, _cx: &App) -> FocusHandle {
656        self.focus_handle.clone()
657    }
658}
659
660impl Item for Zeta2Inspector {
661    type Event = ();
662
663    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
664        "Zeta2 Inspector".into()
665    }
666}
667
668impl EventEmitter<()> for Zeta2Inspector {}
669
670impl Render for Zeta2Inspector {
671    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
672        v_flex()
673            .size_full()
674            .bg(cx.theme().colors().editor_background)
675            .child(
676                h_flex()
677                    .w_full()
678                    .child(
679                        v_flex()
680                            .flex_1()
681                            .p_4()
682                            .h_full()
683                            .justify_between()
684                            .child(self.render_options(window, cx))
685                            .gap_4()
686                            .children(self.render_tabs(cx)),
687                    )
688                    .child(ui::vertical_divider())
689                    .children(self.render_stats()),
690            )
691            .child(self.render_content(cx))
692    }
693}
694
695// Using same approach as commit view
696
697struct ExcerptMetadataFile {
698    title: Arc<RelPath>,
699    worktree_id: WorktreeId,
700    path_style: PathStyle,
701}
702
703impl language::File for ExcerptMetadataFile {
704    fn as_local(&self) -> Option<&dyn language::LocalFile> {
705        None
706    }
707
708    fn disk_state(&self) -> DiskState {
709        DiskState::New
710    }
711
712    fn path(&self) -> &Arc<RelPath> {
713        &self.title
714    }
715
716    fn full_path(&self, _: &App) -> PathBuf {
717        self.title.as_std_path().to_path_buf()
718    }
719
720    fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
721        self.title.file_name().unwrap()
722    }
723
724    fn path_style(&self, _: &App) -> PathStyle {
725        self.path_style
726    }
727
728    fn worktree_id(&self, _: &App) -> WorktreeId {
729        self.worktree_id
730    }
731
732    fn to_proto(&self, _: &App) -> language::proto::File {
733        unimplemented!()
734    }
735
736    fn is_private(&self) -> bool {
737        false
738    }
739}