zeta2_tools.rs

   1mod zeta2_context_view;
   2
   3use std::{str::FromStr, sync::Arc, time::Duration};
   4
   5use client::{Client, UserStore};
   6use cloud_llm_client::predict_edits_v3::PromptFormat;
   7use collections::HashMap;
   8use editor::{Editor, EditorEvent, EditorMode, MultiBuffer};
   9use feature_flags::FeatureFlagAppExt as _;
  10use futures::{FutureExt, StreamExt as _, channel::oneshot, future::Shared};
  11use gpui::{
  12    Empty, Entity, EventEmitter, FocusHandle, Focusable, Subscription, Task, WeakEntity, actions,
  13    prelude::*,
  14};
  15use language::Buffer;
  16use project::{Project, telemetry_snapshot::TelemetrySnapshot};
  17use ui::{ButtonLike, ContextMenu, ContextMenuEntry, DropdownMenu, KeyBinding, prelude::*};
  18use ui_input::InputField;
  19use util::ResultExt;
  20use workspace::{Item, SplitDirection, Workspace};
  21use zeta::{
  22    AgenticContextOptions, ContextMode, DEFAULT_SYNTAX_CONTEXT_OPTIONS, EditPredictionInputs, Zeta,
  23    Zeta2FeatureFlag, ZetaDebugInfo, ZetaEditPredictionDebugInfo, ZetaOptions,
  24};
  25
  26use edit_prediction_context::{EditPredictionContextOptions, EditPredictionExcerptOptions};
  27use zeta2_context_view::Zeta2ContextView;
  28
  29actions!(
  30    dev,
  31    [
  32        /// Opens the edit prediction context view.
  33        OpenZeta2ContextView,
  34        /// Opens the edit prediction inspector.
  35        OpenZeta2Inspector,
  36        /// Rate prediction as positive.
  37        Zeta2RatePredictionPositive,
  38        /// Rate prediction as negative.
  39        Zeta2RatePredictionNegative,
  40    ]
  41);
  42
  43pub fn init(cx: &mut App) {
  44    cx.observe_new(move |workspace: &mut Workspace, _, _cx| {
  45        workspace.register_action_renderer(|div, _, _, cx| {
  46            let has_flag = cx.has_flag::<Zeta2FeatureFlag>();
  47            div.when(has_flag, |div| {
  48                div.on_action(
  49                    cx.listener(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                .on_action(cx.listener(
  68                    move |workspace, _: &OpenZeta2ContextView, window, cx| {
  69                        let project = workspace.project();
  70                        workspace.split_item(
  71                            SplitDirection::Right,
  72                            Box::new(cx.new(|cx| {
  73                                Zeta2ContextView::new(
  74                                    project.clone(),
  75                                    workspace.client(),
  76                                    workspace.user_store(),
  77                                    window,
  78                                    cx,
  79                                )
  80                            })),
  81                            window,
  82                            cx,
  83                        );
  84                    },
  85                ))
  86            })
  87        });
  88    })
  89    .detach();
  90}
  91
  92// TODO show included diagnostics, and events
  93
  94pub struct Zeta2Inspector {
  95    focus_handle: FocusHandle,
  96    project: Entity<Project>,
  97    last_prediction: Option<LastPrediction>,
  98    max_excerpt_bytes_input: Entity<InputField>,
  99    min_excerpt_bytes_input: Entity<InputField>,
 100    cursor_context_ratio_input: Entity<InputField>,
 101    max_prompt_bytes_input: Entity<InputField>,
 102    context_mode: ContextModeState,
 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
 116struct LastPrediction {
 117    prompt_editor: Entity<Editor>,
 118    retrieval_time: Duration,
 119    request_time: Option<Duration>,
 120    buffer: WeakEntity<Buffer>,
 121    position: language::Anchor,
 122    state: LastPredictionState,
 123    inputs: EditPredictionInputs,
 124    project_snapshot: Shared<Task<Arc<TelemetrySnapshot>>>,
 125    _task: Option<Task<()>>,
 126}
 127
 128#[derive(Clone, Copy, PartialEq)]
 129enum Feedback {
 130    Positive,
 131    Negative,
 132}
 133
 134enum LastPredictionState {
 135    Requested,
 136    Success {
 137        model_response_editor: Entity<Editor>,
 138        feedback_editor: Entity<Editor>,
 139        feedback: Option<Feedback>,
 140        request_id: String,
 141    },
 142    Failed {
 143        message: String,
 144    },
 145}
 146
 147impl Zeta2Inspector {
 148    pub fn new(
 149        project: &Entity<Project>,
 150        client: &Arc<Client>,
 151        user_store: &Entity<UserStore>,
 152        window: &mut Window,
 153        cx: &mut Context<Self>,
 154    ) -> Self {
 155        let zeta = Zeta::global(client, user_store, cx);
 156        let mut request_rx = zeta.update(cx, |zeta, _cx| zeta.debug_info());
 157
 158        let receive_task = cx.spawn_in(window, async move |this, cx| {
 159            while let Some(prediction) = request_rx.next().await {
 160                this.update_in(cx, |this, window, cx| {
 161                    this.update_last_prediction(prediction, window, cx)
 162                })
 163                .ok();
 164            }
 165        });
 166
 167        let mut this = Self {
 168            focus_handle: cx.focus_handle(),
 169            project: project.clone(),
 170            last_prediction: None,
 171            max_excerpt_bytes_input: Self::number_input("Max Excerpt Bytes", window, cx),
 172            min_excerpt_bytes_input: Self::number_input("Min Excerpt Bytes", window, cx),
 173            cursor_context_ratio_input: Self::number_input("Cursor Context Ratio", window, cx),
 174            max_prompt_bytes_input: Self::number_input("Max Prompt Bytes", window, cx),
 175            context_mode: ContextModeState::Llm,
 176            zeta: zeta.clone(),
 177            _active_editor_subscription: None,
 178            _update_state_task: Task::ready(()),
 179            _receive_task: receive_task,
 180        };
 181        this.set_options_state(&zeta.read(cx).options().clone(), window, cx);
 182        this
 183    }
 184
 185    fn set_options_state(
 186        &mut self,
 187        options: &ZetaOptions,
 188        window: &mut Window,
 189        cx: &mut Context<Self>,
 190    ) {
 191        let excerpt_options = options.context.excerpt();
 192        self.max_excerpt_bytes_input.update(cx, |input, cx| {
 193            input.set_text(excerpt_options.max_bytes.to_string(), window, cx);
 194        });
 195        self.min_excerpt_bytes_input.update(cx, |input, cx| {
 196            input.set_text(excerpt_options.min_bytes.to_string(), window, cx);
 197        });
 198        self.cursor_context_ratio_input.update(cx, |input, cx| {
 199            input.set_text(
 200                format!(
 201                    "{:.2}",
 202                    excerpt_options.target_before_cursor_over_total_bytes
 203                ),
 204                window,
 205                cx,
 206            );
 207        });
 208        self.max_prompt_bytes_input.update(cx, |input, cx| {
 209            input.set_text(options.max_prompt_bytes.to_string(), window, cx);
 210        });
 211
 212        match &options.context {
 213            ContextMode::Agentic(_) => {
 214                self.context_mode = ContextModeState::Llm;
 215            }
 216            ContextMode::Syntax(_) => {
 217                self.context_mode = ContextModeState::Syntax {
 218                    max_retrieved_declarations: Self::number_input(
 219                        "Max Retrieved Definitions",
 220                        window,
 221                        cx,
 222                    ),
 223                };
 224            }
 225        }
 226        cx.notify();
 227    }
 228
 229    fn set_zeta_options(&mut self, options: ZetaOptions, cx: &mut Context<Self>) {
 230        self.zeta.update(cx, |this, _cx| this.set_options(options));
 231
 232        if let Some(prediction) = self.last_prediction.as_mut() {
 233            if let Some(buffer) = prediction.buffer.upgrade() {
 234                let position = prediction.position;
 235                let project = self.project.clone();
 236                self.zeta.update(cx, |zeta, cx| {
 237                    zeta.refresh_prediction_from_buffer(project, buffer, position, cx)
 238                });
 239                prediction.state = LastPredictionState::Requested;
 240            } else {
 241                self.last_prediction.take();
 242            }
 243        }
 244
 245        cx.notify();
 246    }
 247
 248    fn number_input(
 249        label: &'static str,
 250        window: &mut Window,
 251        cx: &mut Context<Self>,
 252    ) -> Entity<InputField> {
 253        let input = cx.new(|cx| {
 254            InputField::new(window, cx, "")
 255                .label(label)
 256                .label_min_width(px(64.))
 257        });
 258
 259        cx.subscribe_in(
 260            &input.read(cx).editor().clone(),
 261            window,
 262            |this, _, event, _window, cx| {
 263                let EditorEvent::BufferEdited = event else {
 264                    return;
 265                };
 266
 267                fn number_input_value<T: FromStr + Default>(
 268                    input: &Entity<InputField>,
 269                    cx: &App,
 270                ) -> T {
 271                    input
 272                        .read(cx)
 273                        .editor()
 274                        .read(cx)
 275                        .text(cx)
 276                        .parse::<T>()
 277                        .unwrap_or_default()
 278                }
 279
 280                let zeta_options = this.zeta.read(cx).options().clone();
 281
 282                let excerpt_options = EditPredictionExcerptOptions {
 283                    max_bytes: number_input_value(&this.max_excerpt_bytes_input, cx),
 284                    min_bytes: number_input_value(&this.min_excerpt_bytes_input, cx),
 285                    target_before_cursor_over_total_bytes: number_input_value(
 286                        &this.cursor_context_ratio_input,
 287                        cx,
 288                    ),
 289                };
 290
 291                let context = match zeta_options.context {
 292                    ContextMode::Agentic(_context_options) => {
 293                        ContextMode::Agentic(AgenticContextOptions {
 294                            excerpt: excerpt_options,
 295                        })
 296                    }
 297                    ContextMode::Syntax(context_options) => {
 298                        let max_retrieved_declarations = match &this.context_mode {
 299                            ContextModeState::Llm => {
 300                                zeta::DEFAULT_SYNTAX_CONTEXT_OPTIONS.max_retrieved_declarations
 301                            }
 302                            ContextModeState::Syntax {
 303                                max_retrieved_declarations,
 304                            } => number_input_value(max_retrieved_declarations, cx),
 305                        };
 306
 307                        ContextMode::Syntax(EditPredictionContextOptions {
 308                            excerpt: excerpt_options,
 309                            max_retrieved_declarations,
 310                            ..context_options
 311                        })
 312                    }
 313                };
 314
 315                this.set_zeta_options(
 316                    ZetaOptions {
 317                        context,
 318                        max_prompt_bytes: number_input_value(&this.max_prompt_bytes_input, cx),
 319                        max_diagnostic_bytes: zeta_options.max_diagnostic_bytes,
 320                        prompt_format: zeta_options.prompt_format,
 321                        file_indexing_parallelism: zeta_options.file_indexing_parallelism,
 322                        buffer_change_grouping_interval: zeta_options
 323                            .buffer_change_grouping_interval,
 324                    },
 325                    cx,
 326                );
 327            },
 328        )
 329        .detach();
 330        input
 331    }
 332
 333    fn update_last_prediction(
 334        &mut self,
 335        prediction: zeta::ZetaDebugInfo,
 336        window: &mut Window,
 337        cx: &mut Context<Self>,
 338    ) {
 339        self._update_state_task = cx.spawn_in(window, {
 340            let language_registry = self.project.read(cx).languages().clone();
 341            async move |this, cx| {
 342                let mut languages = HashMap::default();
 343                let ZetaDebugInfo::EditPredictionRequested(prediction) = prediction else {
 344                    return;
 345                };
 346                for ext in prediction
 347                    .inputs
 348                    .included_files
 349                    .iter()
 350                    .filter_map(|file| file.path.extension())
 351                {
 352                    if !languages.contains_key(ext) {
 353                        // Most snippets are gonna be the same language,
 354                        // so we think it's fine to do this sequentially for now
 355                        languages.insert(
 356                            ext.to_owned(),
 357                            language_registry
 358                                .language_for_name_or_extension(&ext.to_string_lossy())
 359                                .await
 360                                .ok(),
 361                        );
 362                    }
 363                }
 364
 365                let markdown_language = language_registry
 366                    .language_for_name("Markdown")
 367                    .await
 368                    .log_err();
 369
 370                let json_language = language_registry.language_for_name("Json").await.log_err();
 371
 372                this.update_in(cx, |this, window, cx| {
 373                    let ZetaEditPredictionDebugInfo {
 374                        response_rx,
 375                        position,
 376                        buffer,
 377                        retrieval_time,
 378                        local_prompt,
 379                        ..
 380                    } = prediction;
 381
 382                    let task = cx.spawn_in(window, {
 383                        let markdown_language = markdown_language.clone();
 384                        let json_language = json_language.clone();
 385                        async move |this, cx| {
 386                            let response = response_rx.await;
 387
 388                            this.update_in(cx, |this, window, cx| {
 389                                if let Some(prediction) = this.last_prediction.as_mut() {
 390                                    prediction.state = match response {
 391                                        Ok((Ok(response), request_time)) => {
 392                                            prediction.request_time = Some(request_time);
 393
 394                                            let feedback_editor = cx.new(|cx| {
 395                                                let buffer = cx.new(|cx| {
 396                                                    let mut buffer = Buffer::local("", cx);
 397                                                    buffer.set_language(
 398                                                        markdown_language.clone(),
 399                                                        cx,
 400                                                    );
 401                                                    buffer
 402                                                });
 403                                                let buffer =
 404                                                    cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 405                                                let mut editor = Editor::new(
 406                                                    EditorMode::AutoHeight {
 407                                                        min_lines: 3,
 408                                                        max_lines: None,
 409                                                    },
 410                                                    buffer,
 411                                                    None,
 412                                                    window,
 413                                                    cx,
 414                                                );
 415                                                editor.set_placeholder_text(
 416                                                    "Write feedback here",
 417                                                    window,
 418                                                    cx,
 419                                                );
 420                                                editor.set_show_line_numbers(false, cx);
 421                                                editor.set_show_gutter(false, cx);
 422                                                editor.set_show_scrollbars(false, cx);
 423                                                editor
 424                                            });
 425
 426                                            cx.subscribe_in(
 427                                                &feedback_editor,
 428                                                window,
 429                                                |this, editor, ev, window, cx| match ev {
 430                                                    EditorEvent::BufferEdited => {
 431                                                        if let Some(last_prediction) =
 432                                                            this.last_prediction.as_mut()
 433                                                            && let LastPredictionState::Success {
 434                                                                feedback: feedback_state,
 435                                                                ..
 436                                                            } = &mut last_prediction.state
 437                                                        {
 438                                                            if feedback_state.take().is_some() {
 439                                                                editor.update(cx, |editor, cx| {
 440                                                                    editor.set_placeholder_text(
 441                                                                        "Write feedback here",
 442                                                                        window,
 443                                                                        cx,
 444                                                                    );
 445                                                                });
 446                                                                cx.notify();
 447                                                            }
 448                                                        }
 449                                                    }
 450                                                    _ => {}
 451                                                },
 452                                            )
 453                                            .detach();
 454
 455                                            LastPredictionState::Success {
 456                                                model_response_editor: cx.new(|cx| {
 457                                                    let buffer = cx.new(|cx| {
 458                                                        let mut buffer = Buffer::local(
 459                                                            serde_json::to_string_pretty(&response)
 460                                                                .unwrap_or_default(),
 461                                                            cx,
 462                                                        );
 463                                                        buffer.set_language(json_language, cx);
 464                                                        buffer
 465                                                    });
 466                                                    let buffer = cx.new(|cx| {
 467                                                        MultiBuffer::singleton(buffer, cx)
 468                                                    });
 469                                                    let mut editor = Editor::new(
 470                                                        EditorMode::full(),
 471                                                        buffer,
 472                                                        None,
 473                                                        window,
 474                                                        cx,
 475                                                    );
 476                                                    editor.set_read_only(true);
 477                                                    editor.set_show_line_numbers(false, cx);
 478                                                    editor.set_show_gutter(false, cx);
 479                                                    editor.set_show_scrollbars(false, cx);
 480                                                    editor
 481                                                }),
 482                                                feedback_editor,
 483                                                feedback: None,
 484                                                request_id: response.id.clone(),
 485                                            }
 486                                        }
 487                                        Ok((Err(err), request_time)) => {
 488                                            prediction.request_time = Some(request_time);
 489                                            LastPredictionState::Failed { message: err }
 490                                        }
 491                                        Err(oneshot::Canceled) => LastPredictionState::Failed {
 492                                            message: "Canceled".to_string(),
 493                                        },
 494                                    };
 495                                }
 496                            })
 497                            .ok();
 498                        }
 499                    });
 500
 501                    let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
 502
 503                    this.last_prediction = Some(LastPrediction {
 504                        prompt_editor: cx.new(|cx| {
 505                            let buffer = cx.new(|cx| {
 506                                let mut buffer =
 507                                    Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
 508                                buffer.set_language(markdown_language.clone(), cx);
 509                                buffer
 510                            });
 511                            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 512                            let mut editor =
 513                                Editor::new(EditorMode::full(), buffer, None, window, cx);
 514                            editor.set_read_only(true);
 515                            editor.set_show_line_numbers(false, cx);
 516                            editor.set_show_gutter(false, cx);
 517                            editor.set_show_scrollbars(false, cx);
 518                            editor
 519                        }),
 520                        retrieval_time,
 521                        request_time: None,
 522                        buffer,
 523                        position,
 524                        state: LastPredictionState::Requested,
 525                        project_snapshot: cx
 526                            .foreground_executor()
 527                            .spawn(async move { Arc::new(project_snapshot_task.await) })
 528                            .shared(),
 529                        inputs: prediction.inputs,
 530                        _task: Some(task),
 531                    });
 532                    cx.notify();
 533                })
 534                .ok();
 535            }
 536        });
 537    }
 538
 539    fn handle_rate_positive(
 540        &mut self,
 541        _action: &Zeta2RatePredictionPositive,
 542        window: &mut Window,
 543        cx: &mut Context<Self>,
 544    ) {
 545        self.handle_rate(Feedback::Positive, window, cx);
 546    }
 547
 548    fn handle_rate_negative(
 549        &mut self,
 550        _action: &Zeta2RatePredictionNegative,
 551        window: &mut Window,
 552        cx: &mut Context<Self>,
 553    ) {
 554        self.handle_rate(Feedback::Negative, window, cx);
 555    }
 556
 557    fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
 558        let Some(last_prediction) = self.last_prediction.as_mut() else {
 559            return;
 560        };
 561
 562        let project_snapshot_task = last_prediction.project_snapshot.clone();
 563
 564        cx.spawn_in(window, async move |this, cx| {
 565            let project_snapshot = project_snapshot_task.await;
 566            this.update_in(cx, |this, window, cx| {
 567                let Some(last_prediction) = this.last_prediction.as_mut() else {
 568                    return;
 569                };
 570
 571                let LastPredictionState::Success {
 572                    feedback: feedback_state,
 573                    feedback_editor,
 574                    model_response_editor,
 575                    request_id,
 576                    ..
 577                } = &mut last_prediction.state
 578                else {
 579                    return;
 580                };
 581
 582                *feedback_state = Some(kind);
 583                let text = feedback_editor.update(cx, |feedback_editor, cx| {
 584                    feedback_editor.set_placeholder_text(
 585                        "Submitted. Edit or submit again to change.",
 586                        window,
 587                        cx,
 588                    );
 589                    feedback_editor.text(cx)
 590                });
 591                cx.notify();
 592
 593                cx.defer_in(window, {
 594                    let model_response_editor = model_response_editor.downgrade();
 595                    move |_, window, cx| {
 596                        if let Some(model_response_editor) = model_response_editor.upgrade() {
 597                            model_response_editor.focus_handle(cx).focus(window);
 598                        }
 599                    }
 600                });
 601
 602                let kind = match kind {
 603                    Feedback::Positive => "positive",
 604                    Feedback::Negative => "negative",
 605                };
 606
 607                telemetry::event!(
 608                    "Zeta2 Prediction Rated",
 609                    id = request_id,
 610                    kind = kind,
 611                    text = text,
 612                    request = last_prediction.inputs,
 613                    project_snapshot = project_snapshot,
 614                );
 615            })
 616            .log_err();
 617        })
 618        .detach();
 619    }
 620
 621    fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
 622        v_flex()
 623            .gap_2()
 624            .child(
 625                h_flex()
 626                    .child(Headline::new("Options").size(HeadlineSize::Small))
 627                    .justify_between()
 628                    .child(
 629                        ui::Button::new("reset-options", "Reset")
 630                            .disabled(self.zeta.read(cx).options() == &zeta::DEFAULT_OPTIONS)
 631                            .style(ButtonStyle::Outlined)
 632                            .size(ButtonSize::Large)
 633                            .on_click(cx.listener(|this, _, window, cx| {
 634                                this.set_options_state(&zeta::DEFAULT_OPTIONS, window, cx);
 635                            })),
 636                    ),
 637            )
 638            .child(
 639                v_flex()
 640                    .gap_2()
 641                    .child(
 642                        h_flex()
 643                            .gap_2()
 644                            .items_end()
 645                            .child(self.max_excerpt_bytes_input.clone())
 646                            .child(self.min_excerpt_bytes_input.clone())
 647                            .child(self.cursor_context_ratio_input.clone())
 648                            .child(self.render_context_mode_dropdown(window, cx)),
 649                    )
 650                    .child(
 651                        h_flex()
 652                            .gap_2()
 653                            .items_end()
 654                            .children(match &self.context_mode {
 655                                ContextModeState::Llm => None,
 656                                ContextModeState::Syntax {
 657                                    max_retrieved_declarations,
 658                                } => Some(max_retrieved_declarations.clone()),
 659                            })
 660                            .child(self.max_prompt_bytes_input.clone())
 661                            .child(self.render_prompt_format_dropdown(window, cx)),
 662                    ),
 663            )
 664    }
 665
 666    fn render_context_mode_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
 667        let this = cx.weak_entity();
 668
 669        v_flex()
 670            .gap_1p5()
 671            .child(
 672                Label::new("Context Mode")
 673                    .size(LabelSize::Small)
 674                    .color(Color::Muted),
 675            )
 676            .child(
 677                DropdownMenu::new(
 678                    "ep-ctx-mode",
 679                    match &self.context_mode {
 680                        ContextModeState::Llm => "LLM-based",
 681                        ContextModeState::Syntax { .. } => "Syntax",
 682                    },
 683                    ContextMenu::build(window, cx, move |menu, _window, _cx| {
 684                        menu.item(
 685                            ContextMenuEntry::new("LLM-based")
 686                                .toggleable(
 687                                    IconPosition::End,
 688                                    matches!(self.context_mode, ContextModeState::Llm),
 689                                )
 690                                .handler({
 691                                    let this = this.clone();
 692                                    move |window, cx| {
 693                                        this.update(cx, |this, cx| {
 694                                            let current_options =
 695                                                this.zeta.read(cx).options().clone();
 696                                            match current_options.context.clone() {
 697                                                ContextMode::Agentic(_) => {}
 698                                                ContextMode::Syntax(context_options) => {
 699                                                    let options = ZetaOptions {
 700                                                        context: ContextMode::Agentic(
 701                                                            AgenticContextOptions {
 702                                                                excerpt: context_options.excerpt,
 703                                                            },
 704                                                        ),
 705                                                        ..current_options
 706                                                    };
 707                                                    this.set_options_state(&options, window, cx);
 708                                                    this.set_zeta_options(options, cx);
 709                                                }
 710                                            }
 711                                        })
 712                                        .ok();
 713                                    }
 714                                }),
 715                        )
 716                        .item(
 717                            ContextMenuEntry::new("Syntax")
 718                                .toggleable(
 719                                    IconPosition::End,
 720                                    matches!(self.context_mode, ContextModeState::Syntax { .. }),
 721                                )
 722                                .handler({
 723                                    move |window, cx| {
 724                                        this.update(cx, |this, cx| {
 725                                            let current_options =
 726                                                this.zeta.read(cx).options().clone();
 727                                            match current_options.context.clone() {
 728                                                ContextMode::Agentic(context_options) => {
 729                                                    let options = ZetaOptions {
 730                                                        context: ContextMode::Syntax(
 731                                                            EditPredictionContextOptions {
 732                                                                excerpt: context_options.excerpt,
 733                                                                ..DEFAULT_SYNTAX_CONTEXT_OPTIONS
 734                                                            },
 735                                                        ),
 736                                                        ..current_options
 737                                                    };
 738                                                    this.set_options_state(&options, window, cx);
 739                                                    this.set_zeta_options(options, cx);
 740                                                }
 741                                                ContextMode::Syntax(_) => {}
 742                                            }
 743                                        })
 744                                        .ok();
 745                                    }
 746                                }),
 747                        )
 748                    }),
 749                )
 750                .style(ui::DropdownStyle::Outlined),
 751            )
 752    }
 753
 754    fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
 755        let active_format = self.zeta.read(cx).options().prompt_format;
 756        let this = cx.weak_entity();
 757
 758        v_flex()
 759            .gap_1p5()
 760            .child(
 761                Label::new("Prompt Format")
 762                    .size(LabelSize::Small)
 763                    .color(Color::Muted),
 764            )
 765            .child(
 766                DropdownMenu::new(
 767                    "ep-prompt-format",
 768                    active_format.to_string(),
 769                    ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
 770                        for prompt_format in PromptFormat::iter() {
 771                            menu = menu.item(
 772                                ContextMenuEntry::new(prompt_format.to_string())
 773                                    .toggleable(IconPosition::End, active_format == prompt_format)
 774                                    .handler({
 775                                        let this = this.clone();
 776                                        move |_window, cx| {
 777                                            this.update(cx, |this, cx| {
 778                                                let current_options =
 779                                                    this.zeta.read(cx).options().clone();
 780                                                let options = ZetaOptions {
 781                                                    prompt_format,
 782                                                    ..current_options
 783                                                };
 784                                                this.set_zeta_options(options, cx);
 785                                            })
 786                                            .ok();
 787                                        }
 788                                    }),
 789                            )
 790                        }
 791                        menu
 792                    }),
 793                )
 794                .style(ui::DropdownStyle::Outlined),
 795            )
 796    }
 797
 798    fn render_stats(&self) -> Option<Div> {
 799        let Some(prediction) = self.last_prediction.as_ref() else {
 800            return None;
 801        };
 802
 803        Some(
 804            v_flex()
 805                .p_4()
 806                .gap_2()
 807                .min_w(px(160.))
 808                .child(Headline::new("Stats").size(HeadlineSize::Small))
 809                .child(Self::render_duration(
 810                    "Context retrieval",
 811                    Some(prediction.retrieval_time),
 812                ))
 813                .child(Self::render_duration("Request", prediction.request_time)),
 814        )
 815    }
 816
 817    fn render_duration(name: &'static str, time: Option<Duration>) -> Div {
 818        h_flex()
 819            .gap_1()
 820            .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
 821            .child(match time {
 822                Some(time) => Label::new(if time.as_micros() >= 1000 {
 823                    format!("{} ms", time.as_millis())
 824                } else {
 825                    format!("{} µs", time.as_micros())
 826                })
 827                .size(LabelSize::Small),
 828                None => Label::new("...").size(LabelSize::Small),
 829            })
 830    }
 831
 832    fn render_content(&self, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 833        if !cx.has_flag::<Zeta2FeatureFlag>() {
 834            return Self::render_message("`zeta2` feature flag is not enabled");
 835        }
 836
 837        match self.last_prediction.as_ref() {
 838            None => Self::render_message("No prediction"),
 839            Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
 840        }
 841    }
 842
 843    fn render_message(message: impl Into<SharedString>) -> AnyElement {
 844        v_flex()
 845            .size_full()
 846            .justify_center()
 847            .items_center()
 848            .child(Label::new(message).size(LabelSize::Large))
 849            .into_any()
 850    }
 851
 852    fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
 853        h_flex()
 854            .items_start()
 855            .w_full()
 856            .flex_1()
 857            .border_t_1()
 858            .border_color(cx.theme().colors().border)
 859            .bg(cx.theme().colors().editor_background)
 860            .child(
 861                v_flex()
 862                    .flex_1()
 863                    .gap_2()
 864                    .p_4()
 865                    .h_full()
 866                    .child(
 867                        h_flex()
 868                            .justify_between()
 869                            .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
 870                            .child(match prediction.state {
 871                                LastPredictionState::Requested
 872                                | LastPredictionState::Failed { .. } => ui::Chip::new("Local")
 873                                    .bg_color(cx.theme().status().warning_background)
 874                                    .label_color(Color::Success),
 875                                LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
 876                                    .bg_color(cx.theme().status().success_background)
 877                                    .label_color(Color::Success),
 878                            }),
 879                    )
 880                    .child(prediction.prompt_editor.clone()),
 881            )
 882            .child(ui::vertical_divider())
 883            .child(
 884                v_flex()
 885                    .flex_1()
 886                    .gap_2()
 887                    .h_full()
 888                    .child(
 889                        v_flex()
 890                            .flex_1()
 891                            .gap_2()
 892                            .p_4()
 893                            .child(
 894                                ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall),
 895                            )
 896                            .child(match &prediction.state {
 897                                LastPredictionState::Success {
 898                                    model_response_editor,
 899                                    ..
 900                                } => model_response_editor.clone().into_any_element(),
 901                                LastPredictionState::Requested => v_flex()
 902                                    .gap_2()
 903                                    .child(Label::new("Loading...").buffer_font(cx))
 904                                    .into_any_element(),
 905                                LastPredictionState::Failed { message } => v_flex()
 906                                    .gap_2()
 907                                    .max_w_96()
 908                                    .child(Label::new(message.clone()).buffer_font(cx))
 909                                    .into_any_element(),
 910                            }),
 911                    )
 912                    .child(ui::divider())
 913                    .child(
 914                        if let LastPredictionState::Success {
 915                            feedback_editor,
 916                            feedback: feedback_state,
 917                            ..
 918                        } = &prediction.state
 919                        {
 920                            v_flex()
 921                                .key_context("Zeta2Feedback")
 922                                .on_action(cx.listener(Self::handle_rate_positive))
 923                                .on_action(cx.listener(Self::handle_rate_negative))
 924                                .gap_2()
 925                                .p_2()
 926                                .child(feedback_editor.clone())
 927                                .child(
 928                                    h_flex()
 929                                        .justify_end()
 930                                        .w_full()
 931                                        .child(
 932                                            ButtonLike::new("rate-positive")
 933                                                .when(
 934                                                    *feedback_state == Some(Feedback::Positive),
 935                                                    |this| this.style(ButtonStyle::Filled),
 936                                                )
 937                                                .child(
 938                                                    KeyBinding::for_action(
 939                                                        &Zeta2RatePredictionPositive,
 940                                                        cx,
 941                                                    )
 942                                                    .size(TextSize::Small.rems(cx)),
 943                                                )
 944                                                .child(ui::Icon::new(ui::IconName::ThumbsUp))
 945                                                .on_click(cx.listener(|this, _, window, cx| {
 946                                                    this.handle_rate_positive(
 947                                                        &Zeta2RatePredictionPositive,
 948                                                        window,
 949                                                        cx,
 950                                                    );
 951                                                })),
 952                                        )
 953                                        .child(
 954                                            ButtonLike::new("rate-negative")
 955                                                .when(
 956                                                    *feedback_state == Some(Feedback::Negative),
 957                                                    |this| this.style(ButtonStyle::Filled),
 958                                                )
 959                                                .child(
 960                                                    KeyBinding::for_action(
 961                                                        &Zeta2RatePredictionNegative,
 962                                                        cx,
 963                                                    )
 964                                                    .size(TextSize::Small.rems(cx)),
 965                                                )
 966                                                .child(ui::Icon::new(ui::IconName::ThumbsDown))
 967                                                .on_click(cx.listener(|this, _, window, cx| {
 968                                                    this.handle_rate_negative(
 969                                                        &Zeta2RatePredictionNegative,
 970                                                        window,
 971                                                        cx,
 972                                                    );
 973                                                })),
 974                                        ),
 975                                )
 976                                .into_any()
 977                        } else {
 978                            Empty.into_any_element()
 979                        },
 980                    ),
 981            )
 982    }
 983}
 984
 985impl Focusable for Zeta2Inspector {
 986    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 987        self.focus_handle.clone()
 988    }
 989}
 990
 991impl Item for Zeta2Inspector {
 992    type Event = ();
 993
 994    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 995        "Zeta2 Inspector".into()
 996    }
 997}
 998
 999impl EventEmitter<()> for Zeta2Inspector {}
1000
1001impl Render for Zeta2Inspector {
1002    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1003        v_flex()
1004            .size_full()
1005            .bg(cx.theme().colors().editor_background)
1006            .child(
1007                h_flex()
1008                    .w_full()
1009                    .child(
1010                        v_flex()
1011                            .flex_1()
1012                            .p_4()
1013                            .h_full()
1014                            .justify_between()
1015                            .child(self.render_options(window, cx))
1016                            .gap_4(),
1017                    )
1018                    .child(ui::vertical_divider())
1019                    .children(self.render_stats()),
1020            )
1021            .child(self.render_content(window, cx))
1022    }
1023}