zeta2_tools.rs

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