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_immediate(
 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_immediate(
 464                                                            json_language,
 465                                                            cx,
 466                                                        );
 467                                                        buffer
 468                                                    });
 469                                                    let buffer = cx.new(|cx| {
 470                                                        MultiBuffer::singleton(buffer, cx)
 471                                                    });
 472                                                    let mut editor = Editor::new(
 473                                                        EditorMode::full(),
 474                                                        buffer,
 475                                                        None,
 476                                                        window,
 477                                                        cx,
 478                                                    );
 479                                                    editor.set_read_only(true);
 480                                                    editor.set_show_line_numbers(false, cx);
 481                                                    editor.set_show_gutter(false, cx);
 482                                                    editor.set_show_scrollbars(false, cx);
 483                                                    editor
 484                                                }),
 485                                                feedback_editor,
 486                                                feedback: None,
 487                                                request_id: response.id.clone(),
 488                                            }
 489                                        }
 490                                        Ok((Err(err), request_time)) => {
 491                                            prediction.request_time = Some(request_time);
 492                                            LastPredictionState::Failed { message: err }
 493                                        }
 494                                        Err(oneshot::Canceled) => LastPredictionState::Failed {
 495                                            message: "Canceled".to_string(),
 496                                        },
 497                                    };
 498                                }
 499                            })
 500                            .ok();
 501                        }
 502                    });
 503
 504                    let project_snapshot_task = TelemetrySnapshot::new(&this.project, cx);
 505
 506                    this.last_prediction = Some(LastPrediction {
 507                        prompt_editor: cx.new(|cx| {
 508                            let buffer = cx.new(|cx| {
 509                                let mut buffer =
 510                                    Buffer::local(local_prompt.unwrap_or_else(|err| err), cx);
 511                                buffer.set_language_immediate(markdown_language.clone(), cx);
 512                                buffer
 513                            });
 514                            let buffer = cx.new(|cx| MultiBuffer::singleton(buffer, cx));
 515                            let mut editor =
 516                                Editor::new(EditorMode::full(), buffer, None, window, cx);
 517                            editor.set_read_only(true);
 518                            editor.set_show_line_numbers(false, cx);
 519                            editor.set_show_gutter(false, cx);
 520                            editor.set_show_scrollbars(false, cx);
 521                            editor
 522                        }),
 523                        retrieval_time,
 524                        request_time: None,
 525                        buffer,
 526                        position,
 527                        state: LastPredictionState::Requested,
 528                        project_snapshot: cx
 529                            .foreground_executor()
 530                            .spawn(async move { Arc::new(project_snapshot_task.await) })
 531                            .shared(),
 532                        inputs: prediction.inputs,
 533                        _task: Some(task),
 534                    });
 535                    cx.notify();
 536                })
 537                .ok();
 538            }
 539        });
 540    }
 541
 542    fn handle_rate_positive(
 543        &mut self,
 544        _action: &Zeta2RatePredictionPositive,
 545        window: &mut Window,
 546        cx: &mut Context<Self>,
 547    ) {
 548        self.handle_rate(Feedback::Positive, window, cx);
 549    }
 550
 551    fn handle_rate_negative(
 552        &mut self,
 553        _action: &Zeta2RatePredictionNegative,
 554        window: &mut Window,
 555        cx: &mut Context<Self>,
 556    ) {
 557        self.handle_rate(Feedback::Negative, window, cx);
 558    }
 559
 560    fn handle_rate(&mut self, kind: Feedback, window: &mut Window, cx: &mut Context<Self>) {
 561        let Some(last_prediction) = self.last_prediction.as_mut() else {
 562            return;
 563        };
 564
 565        let project_snapshot_task = last_prediction.project_snapshot.clone();
 566
 567        cx.spawn_in(window, async move |this, cx| {
 568            let project_snapshot = project_snapshot_task.await;
 569            this.update_in(cx, |this, window, cx| {
 570                let Some(last_prediction) = this.last_prediction.as_mut() else {
 571                    return;
 572                };
 573
 574                let LastPredictionState::Success {
 575                    feedback: feedback_state,
 576                    feedback_editor,
 577                    model_response_editor,
 578                    request_id,
 579                    ..
 580                } = &mut last_prediction.state
 581                else {
 582                    return;
 583                };
 584
 585                *feedback_state = Some(kind);
 586                let text = feedback_editor.update(cx, |feedback_editor, cx| {
 587                    feedback_editor.set_placeholder_text(
 588                        "Submitted. Edit or submit again to change.",
 589                        window,
 590                        cx,
 591                    );
 592                    feedback_editor.text(cx)
 593                });
 594                cx.notify();
 595
 596                cx.defer_in(window, {
 597                    let model_response_editor = model_response_editor.downgrade();
 598                    move |_, window, cx| {
 599                        if let Some(model_response_editor) = model_response_editor.upgrade() {
 600                            model_response_editor.focus_handle(cx).focus(window);
 601                        }
 602                    }
 603                });
 604
 605                let kind = match kind {
 606                    Feedback::Positive => "positive",
 607                    Feedback::Negative => "negative",
 608                };
 609
 610                telemetry::event!(
 611                    "Zeta2 Prediction Rated",
 612                    id = request_id,
 613                    kind = kind,
 614                    text = text,
 615                    request = last_prediction.inputs,
 616                    project_snapshot = project_snapshot,
 617                );
 618            })
 619            .log_err();
 620        })
 621        .detach();
 622    }
 623
 624    fn render_options(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
 625        v_flex()
 626            .gap_2()
 627            .child(
 628                h_flex()
 629                    .child(Headline::new("Options").size(HeadlineSize::Small))
 630                    .justify_between()
 631                    .child(
 632                        ui::Button::new("reset-options", "Reset")
 633                            .disabled(self.zeta.read(cx).options() == &zeta::DEFAULT_OPTIONS)
 634                            .style(ButtonStyle::Outlined)
 635                            .size(ButtonSize::Large)
 636                            .on_click(cx.listener(|this, _, window, cx| {
 637                                this.set_options_state(&zeta::DEFAULT_OPTIONS, window, cx);
 638                            })),
 639                    ),
 640            )
 641            .child(
 642                v_flex()
 643                    .gap_2()
 644                    .child(
 645                        h_flex()
 646                            .gap_2()
 647                            .items_end()
 648                            .child(self.max_excerpt_bytes_input.clone())
 649                            .child(self.min_excerpt_bytes_input.clone())
 650                            .child(self.cursor_context_ratio_input.clone())
 651                            .child(self.render_context_mode_dropdown(window, cx)),
 652                    )
 653                    .child(
 654                        h_flex()
 655                            .gap_2()
 656                            .items_end()
 657                            .children(match &self.context_mode {
 658                                ContextModeState::Llm => None,
 659                                ContextModeState::Syntax {
 660                                    max_retrieved_declarations,
 661                                } => Some(max_retrieved_declarations.clone()),
 662                            })
 663                            .child(self.max_prompt_bytes_input.clone())
 664                            .child(self.render_prompt_format_dropdown(window, cx)),
 665                    ),
 666            )
 667    }
 668
 669    fn render_context_mode_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
 670        let this = cx.weak_entity();
 671
 672        v_flex()
 673            .gap_1p5()
 674            .child(
 675                Label::new("Context Mode")
 676                    .size(LabelSize::Small)
 677                    .color(Color::Muted),
 678            )
 679            .child(
 680                DropdownMenu::new(
 681                    "ep-ctx-mode",
 682                    match &self.context_mode {
 683                        ContextModeState::Llm => "LLM-based",
 684                        ContextModeState::Syntax { .. } => "Syntax",
 685                    },
 686                    ContextMenu::build(window, cx, move |menu, _window, _cx| {
 687                        menu.item(
 688                            ContextMenuEntry::new("LLM-based")
 689                                .toggleable(
 690                                    IconPosition::End,
 691                                    matches!(self.context_mode, ContextModeState::Llm),
 692                                )
 693                                .handler({
 694                                    let this = this.clone();
 695                                    move |window, cx| {
 696                                        this.update(cx, |this, cx| {
 697                                            let current_options =
 698                                                this.zeta.read(cx).options().clone();
 699                                            match current_options.context.clone() {
 700                                                ContextMode::Agentic(_) => {}
 701                                                ContextMode::Syntax(context_options) => {
 702                                                    let options = ZetaOptions {
 703                                                        context: ContextMode::Agentic(
 704                                                            AgenticContextOptions {
 705                                                                excerpt: context_options.excerpt,
 706                                                            },
 707                                                        ),
 708                                                        ..current_options
 709                                                    };
 710                                                    this.set_options_state(&options, window, cx);
 711                                                    this.set_zeta_options(options, cx);
 712                                                }
 713                                            }
 714                                        })
 715                                        .ok();
 716                                    }
 717                                }),
 718                        )
 719                        .item(
 720                            ContextMenuEntry::new("Syntax")
 721                                .toggleable(
 722                                    IconPosition::End,
 723                                    matches!(self.context_mode, ContextModeState::Syntax { .. }),
 724                                )
 725                                .handler({
 726                                    move |window, cx| {
 727                                        this.update(cx, |this, cx| {
 728                                            let current_options =
 729                                                this.zeta.read(cx).options().clone();
 730                                            match current_options.context.clone() {
 731                                                ContextMode::Agentic(context_options) => {
 732                                                    let options = ZetaOptions {
 733                                                        context: ContextMode::Syntax(
 734                                                            EditPredictionContextOptions {
 735                                                                excerpt: context_options.excerpt,
 736                                                                ..DEFAULT_SYNTAX_CONTEXT_OPTIONS
 737                                                            },
 738                                                        ),
 739                                                        ..current_options
 740                                                    };
 741                                                    this.set_options_state(&options, window, cx);
 742                                                    this.set_zeta_options(options, cx);
 743                                                }
 744                                                ContextMode::Syntax(_) => {}
 745                                            }
 746                                        })
 747                                        .ok();
 748                                    }
 749                                }),
 750                        )
 751                    }),
 752                )
 753                .style(ui::DropdownStyle::Outlined),
 754            )
 755    }
 756
 757    fn render_prompt_format_dropdown(&self, window: &mut Window, cx: &mut Context<Self>) -> Div {
 758        let active_format = self.zeta.read(cx).options().prompt_format;
 759        let this = cx.weak_entity();
 760
 761        v_flex()
 762            .gap_1p5()
 763            .child(
 764                Label::new("Prompt Format")
 765                    .size(LabelSize::Small)
 766                    .color(Color::Muted),
 767            )
 768            .child(
 769                DropdownMenu::new(
 770                    "ep-prompt-format",
 771                    active_format.to_string(),
 772                    ContextMenu::build(window, cx, move |mut menu, _window, _cx| {
 773                        for prompt_format in PromptFormat::iter() {
 774                            menu = menu.item(
 775                                ContextMenuEntry::new(prompt_format.to_string())
 776                                    .toggleable(IconPosition::End, active_format == prompt_format)
 777                                    .handler({
 778                                        let this = this.clone();
 779                                        move |_window, cx| {
 780                                            this.update(cx, |this, cx| {
 781                                                let current_options =
 782                                                    this.zeta.read(cx).options().clone();
 783                                                let options = ZetaOptions {
 784                                                    prompt_format,
 785                                                    ..current_options
 786                                                };
 787                                                this.set_zeta_options(options, cx);
 788                                            })
 789                                            .ok();
 790                                        }
 791                                    }),
 792                            )
 793                        }
 794                        menu
 795                    }),
 796                )
 797                .style(ui::DropdownStyle::Outlined),
 798            )
 799    }
 800
 801    fn render_stats(&self) -> Option<Div> {
 802        let Some(prediction) = self.last_prediction.as_ref() else {
 803            return None;
 804        };
 805
 806        Some(
 807            v_flex()
 808                .p_4()
 809                .gap_2()
 810                .min_w(px(160.))
 811                .child(Headline::new("Stats").size(HeadlineSize::Small))
 812                .child(Self::render_duration(
 813                    "Context retrieval",
 814                    Some(prediction.retrieval_time),
 815                ))
 816                .child(Self::render_duration("Request", prediction.request_time)),
 817        )
 818    }
 819
 820    fn render_duration(name: &'static str, time: Option<Duration>) -> Div {
 821        h_flex()
 822            .gap_1()
 823            .child(Label::new(name).color(Color::Muted).size(LabelSize::Small))
 824            .child(match time {
 825                Some(time) => Label::new(if time.as_micros() >= 1000 {
 826                    format!("{} ms", time.as_millis())
 827                } else {
 828                    format!("{} µs", time.as_micros())
 829                })
 830                .size(LabelSize::Small),
 831                None => Label::new("...").size(LabelSize::Small),
 832            })
 833    }
 834
 835    fn render_content(&self, _: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 836        if !cx.has_flag::<Zeta2FeatureFlag>() {
 837            return Self::render_message("`zeta2` feature flag is not enabled");
 838        }
 839
 840        match self.last_prediction.as_ref() {
 841            None => Self::render_message("No prediction"),
 842            Some(prediction) => self.render_last_prediction(prediction, cx).into_any(),
 843        }
 844    }
 845
 846    fn render_message(message: impl Into<SharedString>) -> AnyElement {
 847        v_flex()
 848            .size_full()
 849            .justify_center()
 850            .items_center()
 851            .child(Label::new(message).size(LabelSize::Large))
 852            .into_any()
 853    }
 854
 855    fn render_last_prediction(&self, prediction: &LastPrediction, cx: &mut Context<Self>) -> Div {
 856        h_flex()
 857            .items_start()
 858            .w_full()
 859            .flex_1()
 860            .border_t_1()
 861            .border_color(cx.theme().colors().border)
 862            .bg(cx.theme().colors().editor_background)
 863            .child(
 864                v_flex()
 865                    .flex_1()
 866                    .gap_2()
 867                    .p_4()
 868                    .h_full()
 869                    .child(
 870                        h_flex()
 871                            .justify_between()
 872                            .child(ui::Headline::new("Prompt").size(ui::HeadlineSize::XSmall))
 873                            .child(match prediction.state {
 874                                LastPredictionState::Requested
 875                                | LastPredictionState::Failed { .. } => ui::Chip::new("Local")
 876                                    .bg_color(cx.theme().status().warning_background)
 877                                    .label_color(Color::Success),
 878                                LastPredictionState::Success { .. } => ui::Chip::new("Cloud")
 879                                    .bg_color(cx.theme().status().success_background)
 880                                    .label_color(Color::Success),
 881                            }),
 882                    )
 883                    .child(prediction.prompt_editor.clone()),
 884            )
 885            .child(ui::vertical_divider())
 886            .child(
 887                v_flex()
 888                    .flex_1()
 889                    .gap_2()
 890                    .h_full()
 891                    .child(
 892                        v_flex()
 893                            .flex_1()
 894                            .gap_2()
 895                            .p_4()
 896                            .child(
 897                                ui::Headline::new("Model Response").size(ui::HeadlineSize::XSmall),
 898                            )
 899                            .child(match &prediction.state {
 900                                LastPredictionState::Success {
 901                                    model_response_editor,
 902                                    ..
 903                                } => model_response_editor.clone().into_any_element(),
 904                                LastPredictionState::Requested => v_flex()
 905                                    .gap_2()
 906                                    .child(Label::new("Loading...").buffer_font(cx))
 907                                    .into_any_element(),
 908                                LastPredictionState::Failed { message } => v_flex()
 909                                    .gap_2()
 910                                    .max_w_96()
 911                                    .child(Label::new(message.clone()).buffer_font(cx))
 912                                    .into_any_element(),
 913                            }),
 914                    )
 915                    .child(ui::divider())
 916                    .child(
 917                        if let LastPredictionState::Success {
 918                            feedback_editor,
 919                            feedback: feedback_state,
 920                            ..
 921                        } = &prediction.state
 922                        {
 923                            v_flex()
 924                                .key_context("Zeta2Feedback")
 925                                .on_action(cx.listener(Self::handle_rate_positive))
 926                                .on_action(cx.listener(Self::handle_rate_negative))
 927                                .gap_2()
 928                                .p_2()
 929                                .child(feedback_editor.clone())
 930                                .child(
 931                                    h_flex()
 932                                        .justify_end()
 933                                        .w_full()
 934                                        .child(
 935                                            ButtonLike::new("rate-positive")
 936                                                .when(
 937                                                    *feedback_state == Some(Feedback::Positive),
 938                                                    |this| this.style(ButtonStyle::Filled),
 939                                                )
 940                                                .child(
 941                                                    KeyBinding::for_action(
 942                                                        &Zeta2RatePredictionPositive,
 943                                                        cx,
 944                                                    )
 945                                                    .size(TextSize::Small.rems(cx)),
 946                                                )
 947                                                .child(ui::Icon::new(ui::IconName::ThumbsUp))
 948                                                .on_click(cx.listener(|this, _, window, cx| {
 949                                                    this.handle_rate_positive(
 950                                                        &Zeta2RatePredictionPositive,
 951                                                        window,
 952                                                        cx,
 953                                                    );
 954                                                })),
 955                                        )
 956                                        .child(
 957                                            ButtonLike::new("rate-negative")
 958                                                .when(
 959                                                    *feedback_state == Some(Feedback::Negative),
 960                                                    |this| this.style(ButtonStyle::Filled),
 961                                                )
 962                                                .child(
 963                                                    KeyBinding::for_action(
 964                                                        &Zeta2RatePredictionNegative,
 965                                                        cx,
 966                                                    )
 967                                                    .size(TextSize::Small.rems(cx)),
 968                                                )
 969                                                .child(ui::Icon::new(ui::IconName::ThumbsDown))
 970                                                .on_click(cx.listener(|this, _, window, cx| {
 971                                                    this.handle_rate_negative(
 972                                                        &Zeta2RatePredictionNegative,
 973                                                        window,
 974                                                        cx,
 975                                                    );
 976                                                })),
 977                                        ),
 978                                )
 979                                .into_any()
 980                        } else {
 981                            Empty.into_any_element()
 982                        },
 983                    ),
 984            )
 985    }
 986}
 987
 988impl Focusable for Zeta2Inspector {
 989    fn focus_handle(&self, _cx: &App) -> FocusHandle {
 990        self.focus_handle.clone()
 991    }
 992}
 993
 994impl Item for Zeta2Inspector {
 995    type Event = ();
 996
 997    fn tab_content_text(&self, _detail: usize, _cx: &App) -> SharedString {
 998        "Zeta2 Inspector".into()
 999    }
1000}
1001
1002impl EventEmitter<()> for Zeta2Inspector {}
1003
1004impl Render for Zeta2Inspector {
1005    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
1006        v_flex()
1007            .size_full()
1008            .bg(cx.theme().colors().editor_background)
1009            .child(
1010                h_flex()
1011                    .w_full()
1012                    .child(
1013                        v_flex()
1014                            .flex_1()
1015                            .p_4()
1016                            .h_full()
1017                            .justify_between()
1018                            .child(self.render_options(window, cx))
1019                            .gap_4(),
1020                    )
1021                    .child(ui::vertical_divider())
1022                    .children(self.render_stats()),
1023            )
1024            .child(self.render_content(window, cx))
1025    }
1026}