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