zeta2_tools.rs

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