1mod zeta2_context_view;
2
3use std::{cmp::Reverse, path::PathBuf, str::FromStr, sync::Arc, time::Duration};
4
5use chrono::TimeDelta;
6use client::{Client, UserStore};
7use cloud_llm_client::predict_edits_v3::{
8 DeclarationScoreComponents, PredictEditsRequest, PromptFormat,
9};
10use collections::HashMap;
11use editor::{Editor, EditorEvent, EditorMode, ExcerptRange, MultiBuffer};
12use feature_flags::FeatureFlagAppExt as _;
13use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared};
14use gpui::{
15 CursorStyle, Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task,
16 WeakEntity, actions, prelude::*,
17};
18use language::{Buffer, DiskState};
19use ordered_float::OrderedFloat;
20use project::{Project, WorktreeId, telemetry_snapshot::TelemetrySnapshot};
21use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
22use ui_input::InputField;
23use util::{ResultExt, paths::PathStyle, rel_path::RelPath};
24use workspace::{Item, SplitDirection, Workspace};
25use zeta2::{
26 AgenticContextOptions, ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, Zeta, Zeta2FeatureFlag,
27 ZetaDebugInfo, ZetaEditPredictionDebugInfo, ZetaOptions,
28};
29
30use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
31use zeta2_context_view::Zeta2ContextView;
32
33actions!(
34 dev,
35 [
36 /// Opens the edit prediction context view.
37 OpenZeta2ContextView,
38 /// Opens the edit prediction inspector.
39 OpenZeta2Inspector,
40 /// Rate prediction as positive.
41 Zeta2RatePredictionPositive,
42 /// Rate prediction as negative.
43 Zeta2RatePredictionNegative,
44 ]
45);
46
47pub fn init(cx: &mut App) {
48 cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
49 workspace.register_action(move |workspace, _: &OpenZeta2Inspector, window, cx| {
50 let project = workspace.project();
51 workspace.split_item(
52 SplitDirection::Right,
53 Box::new(cx.new(|cx| {
54 Zeta2Inspector::new(
55 &project,
56 workspace.client(),
57 workspace.user_store(),
58 window,
59 cx,
60 )
61 })),
62 window,
63 cx,
64 );
65 });
66 })
67 .detach();
68
69 cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
70 workspace.register_action(move |workspace, _: &OpenZeta2ContextView, window, cx| {
71 let project = workspace.project();
72 workspace.split_item(
73 SplitDirection::Right,
74 Box::new(cx.new(|cx| {
75 Zeta2ContextView::new(
76 project.clone(),
77 workspace.client(),
78 workspace.user_store(),
79 window,
80 cx,
81 )
82 })),
83 window,
84 cx,
85 );
86 });
87 })
88 .detach();
89}
90
91// TODO show included diagnostics, and events
92
93pub struct Zeta2Inspector {
94 focus_handle: FocusHandle,
95 project: Entity<Project>,
96 last_prediction: Option<LastPrediction>,
97 max_excerpt_bytes_input: Entity<InputField>,
98 min_excerpt_bytes_input: Entity<InputField>,
99 cursor_context_ratio_input: Entity<InputField>,
100 max_prompt_bytes_input: Entity<InputField>,
101 context_mode: ContextModeState,
102 active_view: ActiveView,
103 zeta: Entity<Zeta>,
104 _active_editor_subscription: Option<Subscription>,
105 _update_state_task: Task<()>,
106 _receive_task: Task<()>,
107}
108
109pub enum ContextModeState {
110 Llm,
111 Syntax {
112 max_retrieved_declarations: Entity<InputField>,
113 },
114}
115
116#[derive(PartialEq)]
117enum ActiveView {
118 Context,
119 Inference,
120}
121
122struct LastPrediction {
123 context_editor: Entity<Editor>,
124 prompt_editor: Entity<Editor>,
125 retrieval_time: TimeDelta,
126 request_time: Option<TimeDelta>,
127 buffer: WeakEntity<Buffer>,
128 position: language::Anchor,
129 state: LastPredictionState,
130 request: PredictEditsRequest,
131 project_snapshot: Shared<Task<Arc<TelemetrySnapshot>>>,
132 _task: Option<Task<()>>,
133}
134
135#[derive(Clone, Copy, PartialEq)]
136enum Feedback {
137 Positive,
138 Negative,
139}
140
141enum LastPredictionState {
142 Requested,
143 Success {
144 model_response_editor: Entity<Editor>,
145 feedback_editor: Entity<Editor>,
146 feedback: Option<Feedback>,
147 request_id: String,
148 },
149 Failed {
150 message: String,
151 },
152}
153
154impl Zeta2Inspector {
155 pub fn new(
156 project: &Entity<Project>,
157 client: &Arc<Client>,
158 user_store: &Entity<UserStore>,
159 window: &mut Window,
160 cx: &mut Context<Self>,
161 ) -> Self {
162 let zeta = Zeta::global(client, user_store, cx);
163 let mut request_rx = zeta.update(cx, |zeta, _cx| zeta.debug_info());
164
165 let receive_task = cx.spawn_in(window, async move |this, cx| {
166 while let Some(prediction) = request_rx.next().await {
167 this.update_in(cx, |this, window, cx| {
168 this.update_last_prediction(prediction, window, cx)
169 })
170 .ok();
171 }
172 });
173
174 let mut this = Self {
175 focus_handle: cx.focus_handle(),
176 project: project.clone(),
177 last_prediction: None,
178 active_view: ActiveView::Inference,
179 max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
180 min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
181 cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
182 max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx),
183 context_mode: ContextModeState::Llm,
184 zeta: zeta.clone(),
185 _active_editor_subscription: None,
186 _update_state_task: Task::ready(()),
187 _receive_task: receive_task,
188 };
189 this.set_options_state(&zeta.read(cx).options().clone(), window, cx);
190 this
191 }
192
193 fn set_options_state(
194 &mut self,
195 options: &ZetaOptions,
196 window: &mut Window,
197 cx: &mut Context<Self>,
198 ) {
199 let excerpt_options = options.context.excerpt();
200 self.max_excerpt_bytes_input.update(cx, |input, cx| {
201 input.set_text(excerpt_options.max_bytes.to_string(), window, cx);
202 });
203 self.min_excerpt_bytes_input.update(cx, |input, cx| {
204 input.set_text(excerpt_options.min_bytes.to_string(), window, cx);
205 });
206 self.cursor_context_ratio_input.update(cx, |input, cx| {
207 input.set_text(
208 format!(
209 "{:.2}",
210 excerpt_options.target_before_cursor_over_total_bytes
211 ),
212 window,
213 cx,
214 );
215 });
216 self.max_prompt_bytes_input.update(cx, |input, cx| {
217 input.set_text(options.max_prompt_bytes.to_string(), window, cx);
218 });
219
220 match &options.context {
221 ContextMode::Agentic(_) => {
222 self.context_mode = ContextModeState::Llm;
223 }
224 ContextMode::Syntax(_) => {
225 self.context_mode = ContextModeState::Syntax {
226 max_retrieved_declarations: Self::number_input(
227 "Max Retrieved Definitions",
228 window,
229 cx,
230 ),
231 };
232 }
233 }
234 cx.notify();
235 }
236
237 fn set_zeta_options(&mut self, options: ZetaOptions, cx: &mut Context<Self>) {
238 self.zeta.update(cx, |this, _cx| this.set_options(options));
239
240 const DEBOUNCE_TIME: Duration = Duration::from_millis(100);
241
242 if let Some(prediction) = self.last_prediction.as_mut() {
243 if let Some(buffer) = prediction.buffer.upgrade() {
244 let position = prediction.position;
245 let zeta = self.zeta.clone();
246 let project = self.project.clone();
247 prediction._task = Some(cx.spawn(async move |_this, cx| {
248 cx.background_executor().timer(DEBOUNCE_TIME).await;
249 if let Some(task) = zeta
250 .update(cx, |zeta, cx| {
251 zeta.refresh_prediction(&project, &buffer, position, cx)
252 })
253 .ok()
254 {
255 task.await.log_err();
256 }
257 }));
258 prediction.state = LastPredictionState::Requested;
259 } else {
260 self.last_prediction.take();
261 }
262 }
263
264 cx.notify();
265 }
266
267 fn number_input(
268 label: &'static str,
269 window: &mut Window,
270 cx: &mut Context<Self>,
271 ) -> Entity<InputField> {
272 let input = cx.new(|cx| {
273 InputField::new(window, cx, "")
274 .label(label)
275 .label_min_width(px(64.))
276 });
277
278 cx.subscribe_in(
279 &input.read(cx).editor().clone(),
280 window,
281 |this, _, event, _window, cx| {
282 let EditorEvent::BufferEdited = event else {
283 return;
284 };
285
286 fn number_input_value<T: FromStr + Default>(
287 input: &Entity<InputField>,
288 cx: &App,
289 ) -> T {
290 input
291 .read(cx)
292 .editor()
293 .read(cx)
294 .text(cx)
295 .parse::<T>()
296 .unwrap_or_default()
297 }
298
299 let zeta_options = this.zeta.read(cx).options().clone();
300
301 let excerpt_options = EditPredictionExcerptOptions {
302 max_bytes: number_input_value(&this.max_excerpt_bytes_input, cx),
303 min_bytes: number_input_value(&this.min_excerpt_bytes_input, cx),
304 target_before_cursor_over_total_bytes: number_input_value(
305 &this.cursor_context_ratio_input,
306 cx,
307 ),
308 };
309
310 let context = match zeta_options.context {
311 ContextMode::Agentic(_context_options) => {
312 ContextMode::Agentic(AgenticContextOptions {
313 excerpt: excerpt_options,
314 })
315 }
316 ContextMode::Syntax(context_options) => {
317 let max_retrieved_declarations = match &this.context_mode {
318 ContextModeState::Llm => {
319 zeta2::DEFAULT_SYNTAX_CONTEXT_OPTIONS.max_retrieved_declarations
320 }
321 ContextModeState::Syntax {
322 max_retrieved_declarations,
323 } => number_input_value(max_retrieved_declarations, cx),
324 };
325
326 ContextMode::Syntax(EditPredictionContextOptions {
327 excerpt: excerpt_options,
328 max_retrieved_declarations,
329 ..context_options
330 })
331 }
332 };
333
334 this.set_zeta_options(
335 ZetaOptions {
336 context,
337 max_prompt_bytes: number_input_value(&this.max_prompt_bytes_input, cx),
338 max_diagnostic_bytes: zeta_options.max_diagnostic_bytes,
339 prompt_format: zeta_options.prompt_format,
340 file_indexing_parallelism: zeta_options.file_indexing_parallelism,
341 buffer_change_grouping_interval: zeta_options
342 .buffer_change_grouping_interval,
343 },
344 cx,
345 );
346 },
347 )
348 .detach();
349 input
350 }
351
352 fn update_last_prediction(
353 &mut self,
354 prediction: zeta2::ZetaDebugInfo,
355 window: &mut Window,
356 cx: &mut Context<Self>,
357 ) {
358 let project = self.project.read(cx);
359 let path_style = project.path_style(cx);
360 let Some(worktree_id) = project
361 .worktrees(cx)
362 .next()
363 .map(|worktree| worktree.read(cx).id())
364 else {
365 log::error!("Open a worktree to use edit prediction debug view");
366 self.last_prediction.take();
367 return;
368 };
369
370 self._update_state_task = cx.spawn_in(window, {
371 let language_registry = self.project.read(cx).languages().clone();
372 async move |this, cx| {
373 let mut languages = HashMap::default();
374 let ZetaDebugInfo::EditPredictionRequested(prediction) = prediction else {
375 return;
376 };
377 for ext in prediction
378 .request
379 .referenced_declarations
380 .iter()
381 .filter_map(|snippet| snippet.path.extension())
382 .chain(prediction.request.excerpt_path.extension())
383 {
384 if !languages.contains_key(ext) {
385 // Most snippets are gonna be the same language,
386 // so we think it's fine to do this sequentially for now
387 languages.insert(
388 ext.to_owned(),
389 language_registry
390 .language_for_name_or_extension(&ext.to_string_lossy())
391 .await
392 .ok(),
393 );
394 }
395 }
396
397 let markdown_language = language_registry
398 .language_for_name("Markdown")
399 .await
400 .log_err();
401
402 let json_language = language_registry.language_for_name("Json").await.log_err();
403
404 this.update_in(cx, |this, window, cx| {
405 let context_editor = cx.new(|cx| {
406 let mut excerpt_score_components = HashMap::default();
407
408 let multibuffer = cx.new(|cx| {
409 let mut multibuffer = MultiBuffer::new(language::Capability::ReadOnly);
410 let excerpt_file = Arc::new(ExcerptMetadataFile {
411 title: RelPath::unix("Cursor Excerpt").unwrap().into(),
412 path_style,
413 worktree_id,
414 });
415
416 let excerpt_buffer = cx.new(|cx| {
417 let mut buffer =
418 Buffer::local(prediction.request.excerpt.clone(), cx);
419 if let Some(language) = prediction
420 .request
421 .excerpt_path
422 .extension()
423 .and_then(|ext| languages.get(ext))
424 {
425 buffer.set_language(language.clone(), cx);
426 }
427 buffer.file_updated(excerpt_file, cx);
428 buffer
429 });
430
431 multibuffer.push_excerpts(
432 excerpt_buffer,
433 [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
434 cx,
435 );
436
437 let mut declarations =
438 prediction.request.referenced_declarations.clone();
439 declarations.sort_unstable_by_key(|declaration| {
440 Reverse(OrderedFloat(declaration.declaration_score))
441 });
442
443 for snippet in &declarations {
444 let snippet_file = Arc::new(ExcerptMetadataFile {
445 title: RelPath::unix(&format!(
446 "{} (Score: {})",
447 snippet.path.display(),
448 snippet.declaration_score
449 ))
450 .unwrap()
451 .into(),
452 path_style,
453 worktree_id,
454 });
455
456 let excerpt_buffer = cx.new(|cx| {
457 let mut buffer = Buffer::local(snippet.text.clone(), cx);
458 buffer.file_updated(snippet_file, cx);
459 if let Some(ext) = snippet.path.extension()
460 && let Some(language) = languages.get(ext)
461 {
462 buffer.set_language(language.clone(), cx);
463 }
464 buffer
465 });
466
467 let excerpt_ids = multibuffer.push_excerpts(
468 excerpt_buffer,
469 [ExcerptRange::new(text::Anchor::MIN..text::Anchor::MAX)],
470 cx,
471 );
472 let excerpt_id = excerpt_ids.first().unwrap();
473
474 excerpt_score_components
475 .insert(*excerpt_id, snippet.score_components.clone());
476 }
477
478 multibuffer
479 });
480
481 let mut editor =
482 Editor::new(EditorMode::full(), multibuffer, None, window, cx);
483 editor.register_addon(ZetaContextAddon {
484 excerpt_score_components,
485 });
486 editor
487 });
488
489 let ZetaEditPredictionDebugInfo {
490 response_rx,
491 position,
492 buffer,
493 retrieval_time,
494 local_prompt,
495 ..
496 } = prediction;
497
498 let task = cx.spawn_in(window, {
499 let markdown_language = markdown_language.clone();
500 let json_language = json_language.clone();
501 async move |this, cx| {
502 let response = response_rx.await;
503
504 this.update_in(cx, |this, window, cx| {
505 if let Some(prediction) = this.last_prediction.as_mut() {
506 prediction.state = match response {
507 Ok((Ok(response), request_time)) => {
508 prediction.request_time = Some(request_time);
509
510 let feedback_editor = cx.new(|cx| {
511 let buffer = cx.new(|cx| {
512 let mut buffer = Buffer::local("", cx);
513 buffer.set_language(
514 markdown_language.clone(),
515 cx,
516 );
517 buffer
518 });
519 let buffer =
520 cx.new(|cx| MultiBuffer::singleton(buffer, cx));
521 let mut editor = Editor::new(
522 EditorMode::AutoHeight {
523 min_lines: 3,
524 max_lines: None,
525 },
526 buffer,
527 None,
528 window,
529 cx,
530 );
531 editor.set_placeholder_text(
532 "Write feedback here",
533 window,
534 cx,
535 );
536 editor.set_show_line_numbers(false, cx);
537 editor.set_show_gutter(false, cx);
538 editor.set_show_scrollbars(false, cx);
539 editor
540 });
541
542 cx.subscribe_in(
543 &feedback_editor,
544 window,
545 |this, editor, ev, window, cx| match ev {
546 EditorEvent::BufferEdited => {
547 if let Some(last_prediction) =
548 this.last_prediction.as_mut()
549 && let LastPredictionState::Success {
550 feedback: feedback_state,
551 ..
552 } = &mut last_prediction.state
553 {
554 if feedback_state.take().is_some() {
555 editor.update(cx, |editor, cx| {
556 editor.set_placeholder_text(
557 "Write feedback here",
558 window,
559 cx,
560 );
561 });
562 cx.notify();
563 }
564 }
565 }
566 _ => {}
567 },
568 )
569 .detach();
570
571 LastPredictionState::Success {
572 model_response_editor: cx.new(|cx| {
573 let buffer = cx.new(|cx| {
574 let mut buffer = Buffer::local(
575 serde_json::to_string_pretty(&response)
576 .unwrap_or_default(),
577 cx,
578 );
579 buffer.set_language(json_language, cx);
580 buffer
581 });
582 let buffer = cx.new(|cx| {
583 MultiBuffer::singleton(buffer, cx)
584 });
585 let mut editor = Editor::new(
586 EditorMode::full(),
587 buffer,
588 None,
589 window,
590 cx,
591 );
592 editor.set_read_only(true);
593 editor.set_show_line_numbers(false, cx);
594 editor.set_show_gutter(false, cx);
595 editor.set_show_scrollbars(false, cx);
596 editor
597 }),
598 feedback_editor,
599 feedback: None,
600 request_id: response.id.clone(),
601 }
602 }
603 Ok((Err(err), request_time)) => {
604 prediction.request_time = Some(request_time);
605 LastPredictionState::Failed { message: err }
606 }
607 Err(oneshot::Canceled) => LastPredictionState::Failed {
608 message: "Canceled".to_string(),
609 },
610 };
611 }
612 })
613 .ok();
614 }
615 });
616
617 let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
618
619 this.last_prediction = Some(LastPrediction {
620 context_editor,
621 prompt_editor: cx.new(|cx| {
622 let buffer = cx.new(|cx| {
623 let mut buffer =
624 Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
625 buffer.set_language(markdown_language.clone(), cx);
626 buffer
627 });
628 let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
629 let mut editor =
630 Editor::new(EditorMode::full(), buffer, None, window, cx);
631 editor.set_read_only(true);
632 editor.set_show_line_numbers(false, cx);
633 editor.set_show_gutter(false, cx);
634 editor.set_show_scrollbars(false, cx);
635 editor
636 }),
637 retrieval_time,
638 request_time: None,
639 buffer,
640 position,
641 state: LastPredictionState::Requested,
642 project_snapshot: cx
643 .foreground_executor()
644 .spawn(async move { Arc::new(project_snapshot_task.await) })
645 .shared(),
646 request: prediction.request,
647 _task: Some(task),
648 });
649 cx.notify();
650 })
651 .ok();
652 }
653 });
654 }
655
656 fn handle_rate_positive(
657 &mut self,
658 _action: &Zeta2RatePredictionPositive,
659 window: &mut Window,
660 cx: &mut Context<Self>,
661 ) {
662 self.handle_rate(Feedback::Positive, window, cx);
663 }
664
665 fn handle_rate_negative(
666 &mut self,
667 _action: &Zeta2RatePredictionNegative,
668 window: &mut Window,
669 cx: &mut Context<Self>,
670 ) {
671 self.handle_rate(Feedback::Negative, window, cx);
672 }
673
674 fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
675 let Some(last_prediction) = self.last_prediction.as_mut() else {
676 return;
677 };
678 if !last_prediction.request.can_collect_data {
679 return;
680 }
681
682 let project_snapshot_task = last_prediction.project_snapshot.clone();
683
684 cx.spawn_in(window, async move |this, cx| {
685 let project_snapshot = project_snapshot_task.await;
686 this.update_in(cx, |this, window, cx| {
687 let Some(last_prediction) = this.last_prediction.as_mut() else {
688 return;
689 };
690
691 let LastPredictionState::Success {
692 feedback: feedback_state,
693 feedback_editor,
694 model_response_editor,
695 request_id,
696 ..
697 } = &mut last_prediction.state
698 else {
699 return;
700 };
701
702 *feedback_state = Some(kind);
703 let text = feedback_editor.update(cx, |feedback_editor, cx| {
704 feedback_editor.set_placeholder_text(
705 "Submitted. Edit or submit again to change.",
706 window,
707 cx,
708 );
709 feedback_editor.text(cx)
710 });
711 cx.notify();
712
713 cx.defer_in(window, {
714 let model_response_editor = model_response_editor.downgrade();
715 move |_, window, cx| {
716 if let Some(model_response_editor) = model_response_editor.upgrade() {
717 model_response_editor.focus_handle(cx).focus(window);
718 }
719 }
720 });
721
722 let kind = match kind {
723 Feedback::Positive => "positive",
724 Feedback::Negative => "negative",
725 };
726
727 telemetry::event!(
728 "Zeta2 Prediction Rated",
729 id = request_id,
730 kind = kind,
731 text = text,
732 request = last_prediction.request,
733 project_snapshot = project_snapshot,
734 );
735 })
736 .log_err();
737 })
738 .detach();
739 }
740
741 fn focus_feedback(&mut self, window: &mut Window, cx: &mut Context<Self>) {
742 if let Some(last_prediction) = self.last_prediction.as_mut() {
743 if let LastPredictionState::Success {
744 feedback_editor, ..
745 } = &mut last_prediction.state
746 {
747 feedback_editor.focus_handle(cx).focus(window);
748 }
749 };
750 }
751
752 fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
753 v_flex()
754 .gap_2()
755 .child(
756 h_flex()
757 .child(Headline::new("Options").size(HeadlineSize::Small))
758 .justify_between()
759 .child(
760 ui::Button::new("reset-options", "Reset")
761 .disabled(self.zeta.read(cx).options() == &zeta2::DEFAULT_OPTIONS)
762 .style(ButtonStyle::Outlined)
763 .size(ButtonSize::Large)
764 .on_click(cx.listener(|this, _, window, cx| {
765 this.set_options_state(&zeta2::DEFAULT_OPTIONS, window, cx);
766 })),
767 ),
768 )
769 .child(
770 v_flex()
771 .gap_2()
772 .child(
773 h_flex()
774 .gap_2()
775 .items_end()
776 .child(self.max_excerpt_bytes_input.clone())
777 .child(self.min_excerpt_bytes_input.clone())
778 .child(self.cursor_context_ratio_input.clone())
779 .child(self.render_context_mode_dropdown(window, cx)),
780 )
781 .child(
782 h_flex()
783 .gap_2()
784 .items_end()
785 .children(match &self.context_mode {
786 ContextModeState::Llm => None,
787 ContextModeState::Syntax {
788 max_retrieved_declarations,
789 } => Some(max_retrieved_declarations.clone()),
790 })
791 .child(self.max_prompt_bytes_input.clone())
792 .child(self.render_prompt_format_dropdown(window, cx)),
793 ),
794 )
795 }
796
797 fn render_context_mode_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
798 let this = cx.weak_entity();
799
800 v_flex()
801 .gap_1p5()
802 .child(
803 Label::new("Context Mode")
804 .size(LabelSize::Small)
805 .color(Color::Muted),
806 )
807 .child(
808 DropdownMenu::new(
809 "ep-ctx-mode",
810 match &self.context_mode {
811 ContextModeState::Llm => "LLM-based",
812 ContextModeState::Syntax { .. } => "Syntax",
813 },
814 ContextMenu::build(window, cx, move |menu, _window, _cx| {
815 menu.item(
816 ContextMenuEntry::new("LLM-based")
817 .toggleable(
818 IconPosition::End,
819 matches!(self.context_mode, ContextModeState::Llm),
820 )
821 .handler({
822 let this = this.clone();
823 move |window, cx| {
824 this.update(cx, |this, cx| {
825 let current_options =
826 this.zeta.read(cx).options().clone();
827 match current_options.context.clone() {
828 ContextMode::Agentic(_) => {}
829 ContextMode::Syntax(context_options) => {
830 let options = ZetaOptions {
831 context: ContextMode::Agentic(
832 AgenticContextOptions {
833 excerpt: context_options.excerpt,
834 },
835 ),
836 ..current_options
837 };
838 this.set_options_state(&options, window, cx);
839 this.set_zeta_options(options, cx);
840 }
841 }
842 })
843 .ok();
844 }
845 }),
846 )
847 .item(
848 ContextMenuEntry::new("Syntax")
849 .toggleable(
850 IconPosition::End,
851 matches!(self.context_mode, ContextModeState::Syntax { .. }),
852 )
853 .handler({
854 move |window, cx| {
855 this.update(cx, |this, cx| {
856 let current_options =
857 this.zeta.read(cx).options().clone();
858 match current_options.context.clone() {
859 ContextMode::Agentic(context_options) => {
860 let options = ZetaOptions {
861 context: ContextMode::Syntax(
862 EditPredictionContextOptions {
863 excerpt: context_options.excerpt,
864 ..DEFAULT_SYNTAX_CONTEXT_OPTIONS
865 },
866 ),
867 ..current_options
868 };
869 this.set_options_state(&options, window, cx);
870 this.set_zeta_options(options, cx);
871 }
872 ContextMode::Syntax(_) => {}
873 }
874 })
875 .ok();
876 }
877 }),
878 )
879 }),
880 )
881 .style(ui::DropdownStyle::Outlined),
882 )
883 }
884
885 fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
886 let active_format = self.zeta.read(cx).options().prompt_format;
887 let this = cx.weak_entity();
888
889 v_flex()
890 .gap_1p5()
891 .child(
892 Label::new("Prompt Format")
893 .size(LabelSize::Small)
894 .color(Color::Muted),
895 )
896 .child(
897 DropdownMenu::new(
898 "ep-prompt-format",
899 active_format.to_string(),
900 ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
901 for prompt_format in PromptFormat::iter() {
902 menu = menu.item(
903 ContextMenuEntry::new(prompt_format.to_string())
904 .toggleable(IconPosition::End, active_format == prompt_format)
905 .handler({
906 let this = this.clone();
907 move |_window, cx| {
908 this.update(cx, |this, cx| {
909 let current_options =
910 this.zeta.read(cx).options().clone();
911 let options = ZetaOptions {
912 prompt_format,
913 ..current_options
914 };
915 this.set_zeta_options(options, cx);
916 })
917 .ok();
918 }
919 }),
920 )
921 }
922 menu
923 }),
924 )
925 .style(ui::DropdownStyle::Outlined),
926 )
927 }
928
929 fn render_tabs(&self, cx: &mut Context<Self>) -> Option<AnyElement> {
930 if self.last_prediction.is_none() {
931 return None;
932 };
933
934 Some(
935 ui::ToggleButtonGroup::single_row(
936 "prediction",
937 [
938 ui::ToggleButtonSimple::new(
939 "Context",
940 cx.listener(|this, _, _, cx| {
941 this.active_view = ActiveView::Context;
942 cx.notify();
943 }),
944 ),
945 ui::ToggleButtonSimple::new(
946 "Inference",
947 cx.listener(|this, _, window, cx| {
948 this.active_view = ActiveView::Inference;
949 this.focus_feedback(window, cx);
950 cx.notify();
951 }),
952 ),
953 ],
954 )
955 .style(ui::ToggleButtonGroupStyle::Outlined)
956 .selected_index(if self.active_view == ActiveView::Context {
957 0
958 } else {
959 1
960 })
961 .into_any_element(),
962 )
963 }
964
965 fn render_stats(&self) -> Option<Div> {
966 let Some(prediction) = self.last_prediction.as_ref() else {
967 return None;
968 };
969
970 Some(
971 v_flex()
972 .p_4()
973 .gap_2()
974 .min_w(px(160.))
975 .child(Headline::new("Stats").size(HeadlineSize::Small))
976 .child(Self::render_duration(
977 "Context retrieval",
978 Some(prediction.retrieval_time),
979 ))
980 .child(Self::render_duration("Request", prediction.request_time)),
981 )
982 }
983
984 fn render_duration(name: &'static str, time: Option<chrono::TimeDelta>) -> Div {
985 h_flex()
986 .gap_1()
987 .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
988 .child(match time {
989 Some(time) => Label::new(if time.num_microseconds().unwrap_or(0) >= 1000 {
990 format!("{} ms", time.num_milliseconds())
991 } else {
992 format!("{} µs", time.num_microseconds().unwrap_or(0))
993 })
994 .size(LabelSize::Small),
995 None => Label::new("...").size(LabelSize::Small),
996 })
997 }
998
999 fn render_content(&self, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1000 if !cx.has_flag::<Zeta2FeatureFlag>() {
1001 return Self::render_message("`zeta2` feature flag is not enabled");
1002 }
1003
1004 match self.last_prediction.as_ref() {
1005 None => Self::render_message("No prediction"),
1006 Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
1007 }
1008 }
1009
1010 fn render_message(message: impl Into<SharedString>) -> AnyElement {
1011 v_flex()
1012 .size_full()
1013 .justify_center()
1014 .items_center()
1015 .child(Label::new(message).size(LabelSize::Large))
1016 .into_any()
1017 }
1018
1019 fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
1020 match &self.active_view {
1021 ActiveView::Context => div().size_full().child(prediction.context_editor.clone()),
1022 ActiveView::Inference => h_flex()
1023 .items_start()
1024 .w_full()
1025 .flex_1()
1026 .border_t_1()
1027 .border_color(cx.theme().colors().border)
1028 .bg(cx.theme().colors().editor_background)
1029 .child(
1030 v_flex()
1031 .flex_1()
1032 .gap_2()
1033 .p_4()
1034 .h_full()
1035 .child(
1036 h_flex()
1037 .justify_between()
1038 .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
1039 .child(match prediction.state {
1040 LastPredictionState::Requested
1041 | LastPredictionState::Failed { .. } => ui::Chip::new("Local")
1042 .bg_color(cx.theme().status().warning_background)
1043 .label_color(Color::Success),
1044 LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
1045 .bg_color(cx.theme().status().success_background)
1046 .label_color(Color::Success),
1047 }),
1048 )
1049 .child(prediction.prompt_editor.clone()),
1050 )
1051 .child(ui::vertical_divider())
1052 .child(
1053 v_flex()
1054 .flex_1()
1055 .gap_2()
1056 .h_full()
1057 .child(
1058 v_flex()
1059 .flex_1()
1060 .gap_2()
1061 .p_4()
1062 .child(
1063 ui::Headline::new("Model Response")
1064 .size(ui::HeadlineSize::XSmall),
1065 )
1066 .child(match &prediction.state {
1067 LastPredictionState::Success {
1068 model_response_editor,
1069 ..
1070 } => model_response_editor.clone().into_any_element(),
1071 LastPredictionState::Requested => v_flex()
1072 .gap_2()
1073 .child(Label::new("Loading...").buffer_font(cx))
1074 .into_any_element(),
1075 LastPredictionState::Failed { message } => v_flex()
1076 .gap_2()
1077 .max_w_96()
1078 .child(Label::new(message.clone()).buffer_font(cx))
1079 .into_any_element(),
1080 }),
1081 )
1082 .child(ui::divider())
1083 .child(
1084 if prediction.request.can_collect_data
1085 && let LastPredictionState::Success {
1086 feedback_editor,
1087 feedback: feedback_state,
1088 ..
1089 } = &prediction.state
1090 {
1091 v_flex()
1092 .key_context("Zeta2Feedback")
1093 .on_action(cx.listener(Self::handle_rate_positive))
1094 .on_action(cx.listener(Self::handle_rate_negative))
1095 .gap_2()
1096 .p_2()
1097 .child(feedback_editor.clone())
1098 .child(
1099 h_flex()
1100 .justify_end()
1101 .w_full()
1102 .child(
1103 ButtonLike::new("rate-positive")
1104 .when(
1105 *feedback_state == Some(Feedback::Positive),
1106 |this| this.style(ButtonStyle::Filled),
1107 )
1108 .child(
1109 KeyBinding::for_action(
1110 &Zeta2RatePredictionPositive,
1111 cx,
1112 )
1113 .size(TextSize::Small.rems(cx)),
1114 )
1115 .child(ui::Icon::new(ui::IconName::ThumbsUp))
1116 .on_click(cx.listener(
1117 |this, _, window, cx| {
1118 this.handle_rate_positive(
1119 &Zeta2RatePredictionPositive,
1120 window,
1121 cx,
1122 );
1123 },
1124 )),
1125 )
1126 .child(
1127 ButtonLike::new("rate-negative")
1128 .when(
1129 *feedback_state == Some(Feedback::Negative),
1130 |this| this.style(ButtonStyle::Filled),
1131 )
1132 .child(
1133 KeyBinding::for_action(
1134 &Zeta2RatePredictionNegative,
1135 cx,
1136 )
1137 .size(TextSize::Small.rems(cx)),
1138 )
1139 .child(ui::Icon::new(ui::IconName::ThumbsDown))
1140 .on_click(cx.listener(
1141 |this, _, window, cx| {
1142 this.handle_rate_negative(
1143 &Zeta2RatePredictionNegative,
1144 window,
1145 cx,
1146 );
1147 },
1148 )),
1149 ),
1150 )
1151 .into_any()
1152 } else {
1153 Empty.into_any_element()
1154 },
1155 ),
1156 ),
1157 }
1158 }
1159}
1160
1161impl Focusable for Zeta2Inspector {
1162 fn focus_handle(&self, _cx: &App) -> FocusHandle {
1163 self.focus_handle.clone()
1164 }
1165}
1166
1167impl Item for Zeta2Inspector {
1168 type Event = ();
1169
1170 fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
1171 "Zeta2 Inspector".into()
1172 }
1173}
1174
1175impl EventEmitter<()> for Zeta2Inspector {}
1176
1177impl Render for Zeta2Inspector {
1178 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1179 v_flex()
1180 .size_full()
1181 .bg(cx.theme().colors().editor_background)
1182 .child(
1183 h_flex()
1184 .w_full()
1185 .child(
1186 v_flex()
1187 .flex_1()
1188 .p_4()
1189 .h_full()
1190 .justify_between()
1191 .child(self.render_options(window, cx))
1192 .gap_4()
1193 .children(self.render_tabs(cx)),
1194 )
1195 .child(ui::vertical_divider())
1196 .children(self.render_stats()),
1197 )
1198 .child(self.render_content(window, cx))
1199 }
1200}
1201
1202// Using same approach as commit view
1203
1204struct ExcerptMetadataFile {
1205 title: Arc<RelPath>,
1206 worktree_id: WorktreeId,
1207 path_style: PathStyle,
1208}
1209
1210impl language::File for ExcerptMetadataFile {
1211 fn as_local(&self) -> Option<&dyn language::LocalFile> {
1212 None
1213 }
1214
1215 fn disk_state(&self) -> DiskState {
1216 DiskState::New
1217 }
1218
1219 fn path(&self) -> &Arc<RelPath> {
1220 &self.title
1221 }
1222
1223 fn full_path(&self, _: &App) -> PathBuf {
1224 self.title.as_std_path().to_path_buf()
1225 }
1226
1227 fn file_name<'a>(&'a self, _: &'a App) -> &'a str {
1228 self.title.file_name().unwrap()
1229 }
1230
1231 fn path_style(&self, _: &App) -> PathStyle {
1232 self.path_style
1233 }
1234
1235 fn worktree_id(&self, _: &App) -> WorktreeId {
1236 self.worktree_id
1237 }
1238
1239 fn to_proto(&self, _: &App) -> language::proto::File {
1240 unimplemented!()
1241 }
1242
1243 fn is_private(&self) -> bool {
1244 false
1245 }
1246}
1247
1248struct ZetaContextAddon {
1249 excerpt_score_components: HashMap<editor::ExcerptId, DeclarationScoreComponents>,
1250}
1251
1252impl editor::Addon for ZetaContextAddon {
1253 fn to_any(&self) -> &dyn std::any::Any {
1254 self
1255 }
1256
1257 fn render_buffer_header_controls(
1258 &self,
1259 excerpt_info: &multi_buffer::ExcerptInfo,
1260 _window: &Window,
1261 _cx: &App,
1262 ) -> Option<AnyElement> {
1263 let score_components = self.excerpt_score_components.get(&excerpt_info.id)?.clone();
1264
1265 Some(
1266 div()
1267 .id(excerpt_info.id.to_proto() as usize)
1268 .child(ui::Icon::new(IconName::Info))
1269 .cursor(CursorStyle::PointingHand)
1270 .tooltip(move |_, cx| {
1271 cx.new(|_| ScoreComponentsTooltip::new(&score_components))
1272 .into()
1273 })
1274 .into_any(),
1275 )
1276 }
1277}
1278
1279struct ScoreComponentsTooltip {
1280 text: SharedString,
1281}
1282
1283impl ScoreComponentsTooltip {
1284 fn new(components: &DeclarationScoreComponents) -> Self {
1285 Self {
1286 text: format!("{:#?}", components).into(),
1287 }
1288 }
1289}
1290
1291impl Render for ScoreComponentsTooltip {
1292 fn render(&mut self, _: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1293 div().pl_2().pt_2p5().child(
1294 div()
1295 .elevation_2(cx)
1296 .py_1()
1297 .px_2()
1298 .child(ui::Label::new(self.text.clone()).buffer_font(cx)),
1299 )
1300 }
1301}