inline_prompt_editor.rs

   1use crate::ThreadHistory;
   2use agent::ThreadStore;
   3use agent_settings::AgentSettings;
   4use collections::{HashMap, VecDeque};
   5use editor::actions::Paste;
   6use editor::code_context_menus::CodeContextMenu;
   7use editor::display_map::{CreaseId, EditorMargins};
   8use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
   9use editor::{
  10    ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
  11};
  12use fs::Fs;
  13use gpui::{
  14    AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
  15    Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions,
  16};
  17use language_model::{LanguageModel, LanguageModelRegistry};
  18use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
  19use parking_lot::Mutex;
  20use project::Project;
  21use prompt_store::PromptStore;
  22use settings::Settings;
  23use std::cmp;
  24use std::ops::Range;
  25use std::rc::Rc;
  26use std::sync::Arc;
  27use theme::ThemeSettings;
  28use ui::utils::WithRemSize;
  29use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
  30use uuid::Uuid;
  31use workspace::notifications::NotificationId;
  32use workspace::{Toast, Workspace};
  33use zed_actions::{
  34    agent::ToggleModelSelector,
  35    editor::{MoveDown, MoveUp},
  36};
  37
  38use crate::agent_model_selector::AgentModelSelector;
  39use crate::buffer_codegen::{BufferCodegen, CodegenAlternative};
  40use crate::completion_provider::{
  41    PromptCompletionProvider, PromptCompletionProviderDelegate, PromptContextType,
  42};
  43use crate::mention_set::paste_images_as_context;
  44use crate::mention_set::{MentionSet, crease_for_mention};
  45use crate::terminal_codegen::TerminalCodegen;
  46use crate::{
  47    CycleFavoriteModels, CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext,
  48};
  49
  50actions!(inline_assistant, [ThumbsUpResult, ThumbsDownResult]);
  51
  52enum CompletionState {
  53    Pending,
  54    Generated { completion_text: Option<String> },
  55    Rated,
  56}
  57
  58struct SessionState {
  59    session_id: Uuid,
  60    completion: CompletionState,
  61}
  62
  63pub struct PromptEditor<T> {
  64    pub editor: Entity<Editor>,
  65    mode: PromptEditorMode,
  66    mention_set: Entity<MentionSet>,
  67    history: WeakEntity<ThreadHistory>,
  68    prompt_store: Option<Entity<PromptStore>>,
  69    workspace: WeakEntity<Workspace>,
  70    model_selector: Entity<AgentModelSelector>,
  71    edited_since_done: bool,
  72    prompt_history: VecDeque<String>,
  73    prompt_history_ix: Option<usize>,
  74    pending_prompt: String,
  75    _codegen_subscription: Subscription,
  76    editor_subscriptions: Vec<Subscription>,
  77    show_rate_limit_notice: bool,
  78    session_state: SessionState,
  79    _phantom: std::marker::PhantomData<T>,
  80}
  81
  82impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
  83
  84impl<T: 'static> Render for PromptEditor<T> {
  85    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
  86        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
  87        let mut buttons = Vec::new();
  88
  89        const RIGHT_PADDING: Pixels = px(9.);
  90
  91        let (left_gutter_width, right_padding, explanation) = match &self.mode {
  92            PromptEditorMode::Buffer {
  93                id: _,
  94                codegen,
  95                editor_margins,
  96            } => {
  97                let codegen = codegen.read(cx);
  98
  99                if codegen.alternative_count(cx) > 1 {
 100                    buttons.push(self.render_cycle_controls(codegen, cx));
 101                }
 102
 103                let editor_margins = editor_margins.lock();
 104                let gutter = editor_margins.gutter;
 105
 106                let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
 107                let right_padding = editor_margins.right + RIGHT_PADDING;
 108
 109                let active_alternative = codegen.active_alternative().read(cx);
 110                let explanation = active_alternative
 111                    .description
 112                    .clone()
 113                    .or_else(|| active_alternative.failure.clone());
 114
 115                (left_gutter_width, right_padding, explanation)
 116            }
 117            PromptEditorMode::Terminal { .. } => {
 118                // Give the equivalent of the same left-padding that we're using on the right
 119                (Pixels::from(40.0), Pixels::from(24.), None)
 120            }
 121        };
 122
 123        let bottom_padding = match &self.mode {
 124            PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
 125            PromptEditorMode::Terminal { .. } => rems_from_px(4.0),
 126        };
 127
 128        buttons.extend(self.render_buttons(window, cx));
 129
 130        let menu_visible = self.is_completions_menu_visible(cx);
 131        let add_context_button = IconButton::new("add-context", IconName::AtSign)
 132            .icon_size(IconSize::Small)
 133            .icon_color(Color::Muted)
 134            .when(!menu_visible, |this| {
 135                this.tooltip(move |_window, cx| {
 136                    Tooltip::with_meta("Add Context", None, "Or type @ to include context", cx)
 137                })
 138            })
 139            .on_click(cx.listener(move |this, _, window, cx| {
 140                this.trigger_completion_menu(window, cx);
 141            }));
 142
 143        let markdown = window.use_state(cx, |_, cx| Markdown::new("".into(), None, None, cx));
 144
 145        if let Some(explanation) = &explanation {
 146            markdown.update(cx, |markdown, cx| {
 147                markdown.reset(SharedString::from(explanation), cx);
 148            });
 149        }
 150
 151        let explanation_label = self
 152            .render_markdown(markdown, markdown_style(window, cx))
 153            .into_any_element();
 154
 155        v_flex()
 156            .key_context("InlineAssistant")
 157            .capture_action(cx.listener(Self::paste))
 158            .block_mouse_except_scroll()
 159            .size_full()
 160            .pt_0p5()
 161            .pb(bottom_padding)
 162            .pr(right_padding)
 163            .gap_0p5()
 164            .justify_center()
 165            .border_y_1()
 166            .border_color(cx.theme().colors().border)
 167            .bg(cx.theme().colors().editor_background)
 168            .child(
 169                h_flex()
 170                    .on_action(cx.listener(Self::confirm))
 171                    .on_action(cx.listener(Self::cancel))
 172                    .on_action(cx.listener(Self::move_up))
 173                    .on_action(cx.listener(Self::move_down))
 174                    .on_action(cx.listener(Self::thumbs_up))
 175                    .on_action(cx.listener(Self::thumbs_down))
 176                    .capture_action(cx.listener(Self::cycle_prev))
 177                    .capture_action(cx.listener(Self::cycle_next))
 178                    .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
 179                        this.model_selector
 180                            .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
 181                    }))
 182                    .on_action(cx.listener(|this, _: &CycleFavoriteModels, window, cx| {
 183                        this.model_selector.update(cx, |model_selector, cx| {
 184                            model_selector.cycle_favorite_models(window, cx);
 185                        });
 186                    }))
 187                    .child(
 188                        WithRemSize::new(ui_font_size)
 189                            .h_full()
 190                            .w(left_gutter_width)
 191                            .flex()
 192                            .flex_row()
 193                            .flex_shrink_0()
 194                            .items_center()
 195                            .justify_center()
 196                            .gap_1()
 197                            .child(self.render_close_button(cx))
 198                            .map(|el| {
 199                                let CodegenStatus::Error(error) = self.codegen_status(cx) else {
 200                                    return el;
 201                                };
 202
 203                                let error_message = SharedString::from(error.to_string());
 204                                el.child(
 205                                    div()
 206                                        .id("error")
 207                                        .tooltip(Tooltip::text(error_message))
 208                                        .child(
 209                                            Icon::new(IconName::XCircle)
 210                                                .size(IconSize::Small)
 211                                                .color(Color::Error),
 212                                        ),
 213                                )
 214                            }),
 215                    )
 216                    .child(
 217                        h_flex()
 218                            .w_full()
 219                            .justify_between()
 220                            .child(div().flex_1().child(self.render_editor(window, cx)))
 221                            .child(
 222                                WithRemSize::new(ui_font_size)
 223                                    .flex()
 224                                    .flex_row()
 225                                    .items_center()
 226                                    .gap_1()
 227                                    .child(add_context_button)
 228                                    .child(self.model_selector.clone())
 229                                    .children(buttons),
 230                            ),
 231                    ),
 232            )
 233            .when_some(explanation, |this, _| {
 234                this.child(
 235                    h_flex()
 236                        .size_full()
 237                        .justify_center()
 238                        .child(div().w(left_gutter_width + px(6.)))
 239                        .child(
 240                            div()
 241                                .size_full()
 242                                .min_w_0()
 243                                .pt(rems_from_px(3.))
 244                                .pl_0p5()
 245                                .flex_1()
 246                                .border_t_1()
 247                                .border_color(cx.theme().colors().border_variant)
 248                                .child(explanation_label),
 249                        ),
 250                )
 251            })
 252    }
 253}
 254
 255fn markdown_style(window: &Window, cx: &App) -> MarkdownStyle {
 256    let theme_settings = ThemeSettings::get_global(cx);
 257    let colors = cx.theme().colors();
 258    let mut text_style = window.text_style();
 259
 260    text_style.refine(&TextStyleRefinement {
 261        font_family: Some(theme_settings.ui_font.family.clone()),
 262        color: Some(colors.text),
 263        ..Default::default()
 264    });
 265
 266    MarkdownStyle {
 267        base_text_style: text_style.clone(),
 268        syntax: cx.theme().syntax().clone(),
 269        selection_background_color: colors.element_selection_background,
 270        heading_level_styles: Some(HeadingLevelStyles {
 271            h1: Some(TextStyleRefinement {
 272                font_size: Some(rems(1.15).into()),
 273                ..Default::default()
 274            }),
 275            h2: Some(TextStyleRefinement {
 276                font_size: Some(rems(1.1).into()),
 277                ..Default::default()
 278            }),
 279            h3: Some(TextStyleRefinement {
 280                font_size: Some(rems(1.05).into()),
 281                ..Default::default()
 282            }),
 283            h4: Some(TextStyleRefinement {
 284                font_size: Some(rems(1.).into()),
 285                ..Default::default()
 286            }),
 287            h5: Some(TextStyleRefinement {
 288                font_size: Some(rems(0.95).into()),
 289                ..Default::default()
 290            }),
 291            h6: Some(TextStyleRefinement {
 292                font_size: Some(rems(0.875).into()),
 293                ..Default::default()
 294            }),
 295        }),
 296        inline_code: TextStyleRefinement {
 297            font_family: Some(theme_settings.buffer_font.family.clone()),
 298            font_fallbacks: theme_settings.buffer_font.fallbacks.clone(),
 299            font_features: Some(theme_settings.buffer_font.features.clone()),
 300            background_color: Some(colors.editor_foreground.opacity(0.08)),
 301            ..Default::default()
 302        },
 303        ..Default::default()
 304    }
 305}
 306
 307impl<T: 'static> Focusable for PromptEditor<T> {
 308    fn focus_handle(&self, cx: &App) -> FocusHandle {
 309        self.editor.focus_handle(cx)
 310    }
 311}
 312
 313impl<T: 'static> PromptEditor<T> {
 314    const MAX_LINES: u8 = 8;
 315
 316    fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
 317        match &self.mode {
 318            PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
 319            PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
 320        }
 321    }
 322
 323    fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 324        self.editor_subscriptions.clear();
 325        self.editor_subscriptions.push(cx.subscribe_in(
 326            &self.editor,
 327            window,
 328            Self::handle_prompt_editor_events,
 329        ));
 330    }
 331
 332    fn assign_completion_provider(&mut self, cx: &mut Context<Self>) {
 333        self.editor.update(cx, |editor, cx| {
 334            editor.set_completion_provider(Some(Rc::new(PromptCompletionProvider::new(
 335                PromptEditorCompletionProviderDelegate,
 336                cx.weak_entity(),
 337                self.mention_set.clone(),
 338                self.history.clone(),
 339                self.prompt_store.clone(),
 340                self.workspace.clone(),
 341            ))));
 342        });
 343    }
 344
 345    pub fn set_show_cursor_when_unfocused(
 346        &mut self,
 347        show_cursor_when_unfocused: bool,
 348        cx: &mut Context<Self>,
 349    ) {
 350        self.editor.update(cx, |editor, cx| {
 351            editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
 352        });
 353    }
 354
 355    pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 356        let prompt = self.prompt(cx);
 357        let existing_creases = self.editor.update(cx, |editor, cx| {
 358            extract_message_creases(editor, &self.mention_set, window, cx)
 359        });
 360        let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
 361        let mut creases = vec![];
 362        self.editor = cx.new(|cx| {
 363            let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
 364            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 365            editor.set_placeholder_text("Add a prompt…", window, cx);
 366            editor.set_text(prompt, window, cx);
 367            creases = insert_message_creases(&mut editor, &existing_creases, window, cx);
 368
 369            if focus {
 370                window.focus(&editor.focus_handle(cx), cx);
 371            }
 372            editor
 373        });
 374
 375        self.mention_set.update(cx, |mention_set, _cx| {
 376            debug_assert_eq!(
 377                creases.len(),
 378                mention_set.creases().len(),
 379                "Missing creases"
 380            );
 381
 382            let mentions = mention_set
 383                .clear()
 384                .zip(creases)
 385                .map(|((_, value), id)| (id, value))
 386                .collect::<HashMap<_, _>>();
 387            mention_set.set_mentions(mentions);
 388        });
 389
 390        self.assign_completion_provider(cx);
 391        self.subscribe_to_editor(window, cx);
 392    }
 393
 394    pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
 395        let action = match mode {
 396            PromptEditorMode::Buffer { codegen, .. } => {
 397                if codegen.read(cx).is_insertion {
 398                    "Generate"
 399                } else {
 400                    "Transform"
 401                }
 402            }
 403            PromptEditorMode::Terminal { .. } => "Generate",
 404        };
 405
 406        let agent_panel_keybinding =
 407            ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
 408                .map(|keybinding| format!("{keybinding} to chat"))
 409                .unwrap_or_default();
 410
 411        format!("{action}… ({agent_panel_keybinding} ― ↓↑ for history — @ to include context)")
 412    }
 413
 414    pub fn prompt(&self, cx: &App) -> String {
 415        self.editor.read(cx).text(cx)
 416    }
 417
 418    fn paste(&mut self, _: &Paste, window: &mut Window, cx: &mut Context<Self>) {
 419        if inline_assistant_model_supports_images(cx)
 420            && let Some(task) = paste_images_as_context(
 421                self.editor.clone(),
 422                self.mention_set.clone(),
 423                self.workspace.clone(),
 424                window,
 425                cx,
 426            )
 427        {
 428            task.detach();
 429        }
 430    }
 431
 432    fn handle_prompt_editor_events(
 433        &mut self,
 434        editor: &Entity<Editor>,
 435        event: &EditorEvent,
 436        window: &mut Window,
 437        cx: &mut Context<Self>,
 438    ) {
 439        match event {
 440            EditorEvent::Edited { .. } => {
 441                let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 442
 443                self.mention_set
 444                    .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 445
 446                if let Some(workspace) = Workspace::for_window(window, cx) {
 447                    workspace.update(cx, |workspace, cx| {
 448                        let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
 449
 450                        workspace
 451                            .client()
 452                            .telemetry()
 453                            .log_edit_event("inline assist", is_via_ssh);
 454                    });
 455                }
 456                let prompt = snapshot.text();
 457                if self
 458                    .prompt_history_ix
 459                    .is_none_or(|ix| self.prompt_history[ix] != prompt)
 460                {
 461                    self.prompt_history_ix.take();
 462                    self.pending_prompt = prompt;
 463                }
 464
 465                self.edited_since_done = true;
 466                self.session_state.completion = CompletionState::Pending;
 467                cx.notify();
 468            }
 469            EditorEvent::Blurred => {
 470                if self.show_rate_limit_notice {
 471                    self.show_rate_limit_notice = false;
 472                    cx.notify();
 473                }
 474            }
 475            _ => {}
 476        }
 477    }
 478
 479    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 480        self.editor
 481            .read(cx)
 482            .context_menu()
 483            .borrow()
 484            .as_ref()
 485            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 486    }
 487
 488    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 489        self.editor.update(cx, |editor, cx| {
 490            let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 491                matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 492            });
 493
 494            let has_at_sign = {
 495                let snapshot = editor.display_snapshot(cx);
 496                let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 497                let offset = cursor.to_offset(&snapshot);
 498                if offset.0 > 0 {
 499                    snapshot
 500                        .buffer_snapshot()
 501                        .reversed_chars_at(offset)
 502                        .next()
 503                        .map(|sign| sign == '@')
 504                        .unwrap_or(false)
 505                } else {
 506                    false
 507                }
 508            };
 509
 510            if menu_is_open && has_at_sign {
 511                return;
 512            }
 513
 514            editor.insert("@", window, cx);
 515            editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 516        });
 517    }
 518
 519    fn cancel(
 520        &mut self,
 521        _: &editor::actions::Cancel,
 522        _window: &mut Window,
 523        cx: &mut Context<Self>,
 524    ) {
 525        match self.codegen_status(cx) {
 526            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 527                cx.emit(PromptEditorEvent::CancelRequested);
 528            }
 529            CodegenStatus::Pending => {
 530                cx.emit(PromptEditorEvent::StopRequested);
 531            }
 532        }
 533    }
 534
 535    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 536        match self.codegen_status(cx) {
 537            CodegenStatus::Idle => {
 538                self.fire_started_telemetry(cx);
 539                cx.emit(PromptEditorEvent::StartRequested);
 540            }
 541            CodegenStatus::Pending => {}
 542            CodegenStatus::Done => {
 543                if self.edited_since_done {
 544                    self.fire_started_telemetry(cx);
 545                    cx.emit(PromptEditorEvent::StartRequested);
 546                } else {
 547                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 548                }
 549            }
 550            CodegenStatus::Error(_) => {
 551                self.fire_started_telemetry(cx);
 552                cx.emit(PromptEditorEvent::StartRequested);
 553            }
 554        }
 555    }
 556
 557    fn fire_started_telemetry(&self, cx: &Context<Self>) {
 558        let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() else {
 559            return;
 560        };
 561
 562        let model_telemetry_id = model.model.telemetry_id();
 563        let model_provider_id = model.provider.id().to_string();
 564
 565        let (kind, language_name) = match &self.mode {
 566            PromptEditorMode::Buffer { codegen, .. } => {
 567                let codegen = codegen.read(cx);
 568                (
 569                    "inline",
 570                    codegen.language_name(cx).map(|name| name.to_string()),
 571                )
 572            }
 573            PromptEditorMode::Terminal { .. } => ("inline_terminal", None),
 574        };
 575
 576        telemetry::event!(
 577            "Assistant Started",
 578            session_id = self.session_state.session_id.to_string(),
 579            kind = kind,
 580            phase = "started",
 581            model = model_telemetry_id,
 582            model_provider = model_provider_id,
 583            language_name = language_name,
 584        );
 585    }
 586
 587    fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context<Self>) {
 588        match &self.session_state.completion {
 589            CompletionState::Pending => {
 590                self.toast("Can't rate, still generating...", None, cx);
 591                return;
 592            }
 593            CompletionState::Rated => {
 594                self.toast(
 595                    "Already rated this completion",
 596                    Some(self.session_state.session_id),
 597                    cx,
 598                );
 599                return;
 600            }
 601            CompletionState::Generated { completion_text } => {
 602                let model_info = self.model_selector.read(cx).active_model(cx);
 603                let (model_id, use_streaming_tools) = {
 604                    let Some(configured_model) = model_info else {
 605                        self.toast("No configured model", None, cx);
 606                        return;
 607                    };
 608                    (
 609                        configured_model.model.telemetry_id(),
 610                        CodegenAlternative::use_streaming_tools(
 611                            configured_model.model.as_ref(),
 612                            cx,
 613                        ),
 614                    )
 615                };
 616
 617                let selected_text = match &self.mode {
 618                    PromptEditorMode::Buffer { codegen, .. } => {
 619                        codegen.read(cx).selected_text(cx).map(|s| s.to_string())
 620                    }
 621                    PromptEditorMode::Terminal { .. } => None,
 622                };
 623
 624                let prompt = self.editor.read(cx).text(cx);
 625
 626                let kind = match &self.mode {
 627                    PromptEditorMode::Buffer { .. } => "inline",
 628                    PromptEditorMode::Terminal { .. } => "inline_terminal",
 629                };
 630
 631                telemetry::event!(
 632                    "Inline Assistant Rated",
 633                    rating = "positive",
 634                    session_id = self.session_state.session_id.to_string(),
 635                    kind = kind,
 636                    model = model_id,
 637                    prompt = prompt,
 638                    completion = completion_text,
 639                    selected_text = selected_text,
 640                    use_streaming_tools
 641                );
 642
 643                self.session_state.completion = CompletionState::Rated;
 644
 645                cx.notify();
 646            }
 647        }
 648    }
 649
 650    fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context<Self>) {
 651        match &self.session_state.completion {
 652            CompletionState::Pending => {
 653                self.toast("Can't rate, still generating...", None, cx);
 654                return;
 655            }
 656            CompletionState::Rated => {
 657                self.toast(
 658                    "Already rated this completion",
 659                    Some(self.session_state.session_id),
 660                    cx,
 661                );
 662                return;
 663            }
 664            CompletionState::Generated { completion_text } => {
 665                let model_info = self.model_selector.read(cx).active_model(cx);
 666                let (model_telemetry_id, use_streaming_tools) = {
 667                    let Some(configured_model) = model_info else {
 668                        self.toast("No configured model", None, cx);
 669                        return;
 670                    };
 671                    (
 672                        configured_model.model.telemetry_id(),
 673                        CodegenAlternative::use_streaming_tools(
 674                            configured_model.model.as_ref(),
 675                            cx,
 676                        ),
 677                    )
 678                };
 679
 680                let selected_text = match &self.mode {
 681                    PromptEditorMode::Buffer { codegen, .. } => {
 682                        codegen.read(cx).selected_text(cx).map(|s| s.to_string())
 683                    }
 684                    PromptEditorMode::Terminal { .. } => None,
 685                };
 686
 687                let prompt = self.editor.read(cx).text(cx);
 688
 689                let kind = match &self.mode {
 690                    PromptEditorMode::Buffer { .. } => "inline",
 691                    PromptEditorMode::Terminal { .. } => "inline_terminal",
 692                };
 693
 694                telemetry::event!(
 695                    "Inline Assistant Rated",
 696                    rating = "negative",
 697                    session_id = self.session_state.session_id.to_string(),
 698                    kind = kind,
 699                    model = model_telemetry_id,
 700                    prompt = prompt,
 701                    completion = completion_text,
 702                    selected_text = selected_text,
 703                    use_streaming_tools
 704                );
 705
 706                self.session_state.completion = CompletionState::Rated;
 707
 708                cx.notify();
 709            }
 710        }
 711    }
 712
 713    fn toast(&mut self, msg: &str, uuid: Option<Uuid>, cx: &mut Context<'_, PromptEditor<T>>) {
 714        self.workspace
 715            .update(cx, |workspace, cx| {
 716                enum InlinePromptRating {}
 717                workspace.show_toast(
 718                    {
 719                        let mut toast = Toast::new(
 720                            NotificationId::unique::<InlinePromptRating>(),
 721                            msg.to_string(),
 722                        )
 723                        .autohide();
 724
 725                        if let Some(uuid) = uuid {
 726                            toast = toast.on_click("Click to copy rating ID", move |_, cx| {
 727                                cx.write_to_clipboard(ClipboardItem::new_string(uuid.to_string()));
 728                            });
 729                        };
 730
 731                        toast
 732                    },
 733                    cx,
 734                );
 735            })
 736            .ok();
 737    }
 738
 739    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 740        if let Some(ix) = self.prompt_history_ix {
 741            if ix > 0 {
 742                self.prompt_history_ix = Some(ix - 1);
 743                let prompt = self.prompt_history[ix - 1].as_str();
 744                self.editor.update(cx, |editor, cx| {
 745                    editor.set_text(prompt, window, cx);
 746                    editor.move_to_beginning(&Default::default(), window, cx);
 747                });
 748            }
 749        } else if !self.prompt_history.is_empty() {
 750            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 751            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 752            self.editor.update(cx, |editor, cx| {
 753                editor.set_text(prompt, window, cx);
 754                editor.move_to_beginning(&Default::default(), window, cx);
 755            });
 756        }
 757    }
 758
 759    fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
 760        if let Some(ix) = self.prompt_history_ix {
 761            if ix < self.prompt_history.len() - 1 {
 762                self.prompt_history_ix = Some(ix + 1);
 763                let prompt = self.prompt_history[ix + 1].as_str();
 764                self.editor.update(cx, |editor, cx| {
 765                    editor.set_text(prompt, window, cx);
 766                    editor.move_to_end(&Default::default(), window, cx)
 767                });
 768            } else {
 769                self.prompt_history_ix = None;
 770                let prompt = self.pending_prompt.as_str();
 771                self.editor.update(cx, |editor, cx| {
 772                    editor.set_text(prompt, window, cx);
 773                    editor.move_to_end(&Default::default(), window, cx)
 774                });
 775            }
 776        }
 777    }
 778
 779    fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
 780        let mode = match &self.mode {
 781            PromptEditorMode::Buffer { codegen, .. } => {
 782                let codegen = codegen.read(cx);
 783                if codegen.is_insertion {
 784                    GenerationMode::Generate
 785                } else {
 786                    GenerationMode::Transform
 787                }
 788            }
 789            PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
 790        };
 791
 792        let codegen_status = self.codegen_status(cx);
 793
 794        match codegen_status {
 795            CodegenStatus::Idle => {
 796                vec![
 797                    Button::new("start", mode.start_label())
 798                        .label_size(LabelSize::Small)
 799                        .icon(IconName::Return)
 800                        .icon_size(IconSize::XSmall)
 801                        .icon_color(Color::Muted)
 802                        .on_click(
 803                            cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
 804                        )
 805                        .into_any_element(),
 806                ]
 807            }
 808            CodegenStatus::Pending => vec![
 809                IconButton::new("stop", IconName::Stop)
 810                    .icon_color(Color::Error)
 811                    .shape(IconButtonShape::Square)
 812                    .tooltip(move |_window, cx| {
 813                        Tooltip::with_meta(
 814                            mode.tooltip_interrupt(),
 815                            Some(&menu::Cancel),
 816                            "Changes won't be discarded",
 817                            cx,
 818                        )
 819                    })
 820                    .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 821                    .into_any_element(),
 822            ],
 823            CodegenStatus::Done | CodegenStatus::Error(_) => {
 824                let has_error = matches!(codegen_status, CodegenStatus::Error(_));
 825                if has_error || self.edited_since_done {
 826                    vec![
 827                        IconButton::new("restart", IconName::RotateCw)
 828                            .icon_color(Color::Info)
 829                            .shape(IconButtonShape::Square)
 830                            .tooltip(move |_window, cx| {
 831                                Tooltip::with_meta(
 832                                    mode.tooltip_restart(),
 833                                    Some(&menu::Confirm),
 834                                    "Changes will be discarded",
 835                                    cx,
 836                                )
 837                            })
 838                            .on_click(cx.listener(|_, _, _, cx| {
 839                                cx.emit(PromptEditorEvent::StartRequested);
 840                            }))
 841                            .into_any_element(),
 842                    ]
 843                } else {
 844                    let rated = matches!(self.session_state.completion, CompletionState::Rated);
 845
 846                    let accept = IconButton::new("accept", IconName::Check)
 847                        .icon_color(Color::Info)
 848                        .shape(IconButtonShape::Square)
 849                        .tooltip(move |_window, cx| {
 850                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
 851                        })
 852                        .on_click(cx.listener(|_, _, _, cx| {
 853                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 854                        }))
 855                        .into_any_element();
 856
 857                    let mut buttons = Vec::new();
 858
 859                    if AgentSettings::get_global(cx).enable_feedback {
 860                        buttons.push(
 861                            h_flex()
 862                                .pl_1()
 863                                .gap_1()
 864                                .border_l_1()
 865                                .border_color(cx.theme().colors().border_variant)
 866                                .child(
 867                                    IconButton::new("thumbs-up", IconName::ThumbsUp)
 868                                        .shape(IconButtonShape::Square)
 869                                        .map(|this| {
 870                                            if rated {
 871                                                this.disabled(true)
 872                                                    .icon_color(Color::Disabled)
 873                                                    .tooltip(move |_, cx| {
 874                                                        Tooltip::with_meta(
 875                                                            "Good Result",
 876                                                            None,
 877                                                            "You already rated this result",
 878                                                            cx,
 879                                                        )
 880                                                    })
 881                                            } else {
 882                                                this.icon_color(Color::Muted).tooltip(
 883                                                    move |_, cx| {
 884                                                        Tooltip::for_action(
 885                                                            "Good Result",
 886                                                            &ThumbsUpResult,
 887                                                            cx,
 888                                                        )
 889                                                    },
 890                                                )
 891                                            }
 892                                        })
 893                                        .on_click(cx.listener(|this, _, window, cx| {
 894                                            this.thumbs_up(&ThumbsUpResult, window, cx);
 895                                        })),
 896                                )
 897                                .child(
 898                                    IconButton::new("thumbs-down", IconName::ThumbsDown)
 899                                        .shape(IconButtonShape::Square)
 900                                        .map(|this| {
 901                                            if rated {
 902                                                this.disabled(true)
 903                                                    .icon_color(Color::Disabled)
 904                                                    .tooltip(move |_, cx| {
 905                                                        Tooltip::with_meta(
 906                                                            "Bad Result",
 907                                                            None,
 908                                                            "You already rated this result",
 909                                                            cx,
 910                                                        )
 911                                                    })
 912                                            } else {
 913                                                this.icon_color(Color::Muted).tooltip(
 914                                                    move |_, cx| {
 915                                                        Tooltip::for_action(
 916                                                            "Bad Result",
 917                                                            &ThumbsDownResult,
 918                                                            cx,
 919                                                        )
 920                                                    },
 921                                                )
 922                                            }
 923                                        })
 924                                        .on_click(cx.listener(|this, _, window, cx| {
 925                                            this.thumbs_down(&ThumbsDownResult, window, cx);
 926                                        })),
 927                                )
 928                                .into_any_element(),
 929                        );
 930                    }
 931
 932                    buttons.push(accept);
 933
 934                    match &self.mode {
 935                        PromptEditorMode::Terminal { .. } => {
 936                            buttons.push(
 937                                IconButton::new("confirm", IconName::PlayFilled)
 938                                    .icon_color(Color::Info)
 939                                    .shape(IconButtonShape::Square)
 940                                    .tooltip(|_window, cx| {
 941                                        Tooltip::for_action(
 942                                            "Execute Generated Command",
 943                                            &menu::SecondaryConfirm,
 944                                            cx,
 945                                        )
 946                                    })
 947                                    .on_click(cx.listener(|_, _, _, cx| {
 948                                        cx.emit(PromptEditorEvent::ConfirmRequested {
 949                                            execute: true,
 950                                        });
 951                                    }))
 952                                    .into_any_element(),
 953                            );
 954                            buttons
 955                        }
 956                        PromptEditorMode::Buffer { .. } => buttons,
 957                    }
 958                }
 959            }
 960        }
 961    }
 962
 963    fn cycle_prev(
 964        &mut self,
 965        _: &CyclePreviousInlineAssist,
 966        _: &mut Window,
 967        cx: &mut Context<Self>,
 968    ) {
 969        match &self.mode {
 970            PromptEditorMode::Buffer { codegen, .. } => {
 971                codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
 972            }
 973            PromptEditorMode::Terminal { .. } => {
 974                // no cycle buttons in terminal mode
 975            }
 976        }
 977    }
 978
 979    fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
 980        match &self.mode {
 981            PromptEditorMode::Buffer { codegen, .. } => {
 982                codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
 983            }
 984            PromptEditorMode::Terminal { .. } => {
 985                // no cycle buttons in terminal mode
 986            }
 987        }
 988    }
 989
 990    fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
 991        let focus_handle = self.editor.focus_handle(cx);
 992
 993        IconButton::new("cancel", IconName::Close)
 994            .icon_color(Color::Muted)
 995            .shape(IconButtonShape::Square)
 996            .tooltip({
 997                move |_window, cx| {
 998                    Tooltip::for_action_in(
 999                        "Close Assistant",
1000                        &editor::actions::Cancel,
1001                        &focus_handle,
1002                        cx,
1003                    )
1004                }
1005            })
1006            .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
1007            .into_any_element()
1008    }
1009
1010    fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
1011        let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
1012
1013        let model_registry = LanguageModelRegistry::read_global(cx);
1014        let default_model = model_registry.default_model().map(|default| default.model);
1015        let alternative_models = model_registry.inline_alternative_models();
1016
1017        let get_model_name = |index: usize| -> String {
1018            let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
1019
1020            match index {
1021                0 => default_model.as_ref().map_or_else(String::new, name),
1022                index if index <= alternative_models.len() => alternative_models
1023                    .get(index - 1)
1024                    .map_or_else(String::new, name),
1025                _ => String::new(),
1026            }
1027        };
1028
1029        let total_models = alternative_models.len() + 1;
1030
1031        if total_models <= 1 {
1032            return div().into_any_element();
1033        }
1034
1035        let current_index = codegen.active_alternative;
1036        let prev_index = (current_index + total_models - 1) % total_models;
1037        let next_index = (current_index + 1) % total_models;
1038
1039        let prev_model_name = get_model_name(prev_index);
1040        let next_model_name = get_model_name(next_index);
1041
1042        h_flex()
1043            .child(
1044                IconButton::new("previous", IconName::ChevronLeft)
1045                    .icon_color(Color::Muted)
1046                    .disabled(disabled || current_index == 0)
1047                    .shape(IconButtonShape::Square)
1048                    .tooltip({
1049                        let focus_handle = self.editor.focus_handle(cx);
1050                        move |_window, cx| {
1051                            cx.new(|cx| {
1052                                let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
1053                                    KeyBinding::for_action_in(
1054                                        &CyclePreviousInlineAssist,
1055                                        &focus_handle,
1056                                        cx,
1057                                    ),
1058                                );
1059                                if !disabled && current_index != 0 {
1060                                    tooltip = tooltip.meta(prev_model_name.clone());
1061                                }
1062                                tooltip
1063                            })
1064                            .into()
1065                        }
1066                    })
1067                    .on_click(cx.listener(|this, _, window, cx| {
1068                        this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
1069                    })),
1070            )
1071            .child(
1072                Label::new(format!(
1073                    "{}/{}",
1074                    codegen.active_alternative + 1,
1075                    codegen.alternative_count(cx)
1076                ))
1077                .size(LabelSize::Small)
1078                .color(if disabled {
1079                    Color::Disabled
1080                } else {
1081                    Color::Muted
1082                }),
1083            )
1084            .child(
1085                IconButton::new("next", IconName::ChevronRight)
1086                    .icon_color(Color::Muted)
1087                    .disabled(disabled || current_index == total_models - 1)
1088                    .shape(IconButtonShape::Square)
1089                    .tooltip({
1090                        let focus_handle = self.editor.focus_handle(cx);
1091                        move |_window, cx| {
1092                            cx.new(|cx| {
1093                                let mut tooltip = Tooltip::new("Next Alternative").key_binding(
1094                                    KeyBinding::for_action_in(
1095                                        &CycleNextInlineAssist,
1096                                        &focus_handle,
1097                                        cx,
1098                                    ),
1099                                );
1100                                if !disabled && current_index != total_models - 1 {
1101                                    tooltip = tooltip.meta(next_model_name.clone());
1102                                }
1103                                tooltip
1104                            })
1105                            .into()
1106                        }
1107                    })
1108                    .on_click(cx.listener(|this, _, window, cx| {
1109                        this.cycle_next(&CycleNextInlineAssist, window, cx)
1110                    })),
1111            )
1112            .into_any_element()
1113    }
1114
1115    fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1116        let colors = cx.theme().colors();
1117
1118        div()
1119            .size_full()
1120            .p_2()
1121            .pl_1()
1122            .bg(colors.editor_background)
1123            .child({
1124                let settings = ThemeSettings::get_global(cx);
1125                let font_size = settings.buffer_font_size(cx);
1126
1127                let text_style = TextStyle {
1128                    color: colors.editor_foreground,
1129                    font_family: settings.buffer_font.family.clone(),
1130                    font_features: settings.buffer_font.features.clone(),
1131                    font_size: font_size.into(),
1132                    line_height: relative(settings.buffer_line_height.value()),
1133                    ..Default::default()
1134                };
1135
1136                EditorElement::new(
1137                    &self.editor,
1138                    EditorStyle {
1139                        background: colors.editor_background,
1140                        local_player: cx.theme().players().local(),
1141                        syntax: cx.theme().syntax().clone(),
1142                        text: text_style,
1143                        ..Default::default()
1144                    },
1145                )
1146            })
1147            .into_any_element()
1148    }
1149
1150    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1151        MarkdownElement::new(markdown, style)
1152    }
1153}
1154
1155pub enum PromptEditorMode {
1156    Buffer {
1157        id: InlineAssistId,
1158        codegen: Entity<BufferCodegen>,
1159        editor_margins: Arc<Mutex<EditorMargins>>,
1160    },
1161    Terminal {
1162        id: TerminalInlineAssistId,
1163        codegen: Entity<TerminalCodegen>,
1164        height_in_lines: u8,
1165    },
1166}
1167
1168pub enum PromptEditorEvent {
1169    StartRequested,
1170    StopRequested,
1171    ConfirmRequested { execute: bool },
1172    CancelRequested,
1173    Resized { height_in_lines: u8 },
1174}
1175
1176#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1177pub struct InlineAssistId(pub usize);
1178
1179impl InlineAssistId {
1180    pub fn post_inc(&mut self) -> InlineAssistId {
1181        let id = *self;
1182        self.0 += 1;
1183        id
1184    }
1185}
1186
1187struct PromptEditorCompletionProviderDelegate;
1188
1189fn inline_assistant_model_supports_images(cx: &App) -> bool {
1190    LanguageModelRegistry::read_global(cx)
1191        .inline_assistant_model()
1192        .map_or(false, |m| m.model.supports_images())
1193}
1194
1195impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
1196    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
1197        vec![
1198            PromptContextType::File,
1199            PromptContextType::Symbol,
1200            PromptContextType::Thread,
1201            PromptContextType::Fetch,
1202            PromptContextType::Rules,
1203        ]
1204    }
1205
1206    fn supports_images(&self, cx: &App) -> bool {
1207        inline_assistant_model_supports_images(cx)
1208    }
1209
1210    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
1211        Vec::new()
1212    }
1213
1214    fn confirm_command(&self, _cx: &mut App) {}
1215}
1216
1217impl PromptEditor<BufferCodegen> {
1218    pub fn new_buffer(
1219        id: InlineAssistId,
1220        editor_margins: Arc<Mutex<EditorMargins>>,
1221        prompt_history: VecDeque<String>,
1222        prompt_buffer: Entity<MultiBuffer>,
1223        codegen: Entity<BufferCodegen>,
1224        session_id: Uuid,
1225        fs: Arc<dyn Fs>,
1226        thread_store: Entity<ThreadStore>,
1227        prompt_store: Option<Entity<PromptStore>>,
1228        history: WeakEntity<ThreadHistory>,
1229        project: WeakEntity<Project>,
1230        workspace: WeakEntity<Workspace>,
1231        window: &mut Window,
1232        cx: &mut Context<PromptEditor<BufferCodegen>>,
1233    ) -> PromptEditor<BufferCodegen> {
1234        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1235        let mode = PromptEditorMode::Buffer {
1236            id,
1237            codegen,
1238            editor_margins,
1239        };
1240
1241        let prompt_editor = cx.new(|cx| {
1242            let mut editor = Editor::new(
1243                EditorMode::AutoHeight {
1244                    min_lines: 1,
1245                    max_lines: Some(Self::MAX_LINES as usize),
1246                },
1247                prompt_buffer,
1248                None,
1249                window,
1250                cx,
1251            );
1252            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1253            // Since the prompt editors for all inline assistants are linked,
1254            // always show the cursor (even when it isn't focused) because
1255            // typing in one will make what you typed appear in all of them.
1256            editor.set_show_cursor_when_unfocused(true, cx);
1257            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1258            editor.set_context_menu_options(ContextMenuOptions {
1259                min_entries_visible: 12,
1260                max_entries_visible: 12,
1261                placement: None,
1262            });
1263
1264            editor
1265        });
1266
1267        let mention_set = cx
1268            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1269
1270        let model_selector_menu_handle = PopoverMenuHandle::default();
1271
1272        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
1273            editor: prompt_editor.clone(),
1274            mention_set,
1275            history,
1276            prompt_store,
1277            workspace,
1278            model_selector: cx.new(|cx| {
1279                AgentModelSelector::new(
1280                    fs,
1281                    model_selector_menu_handle,
1282                    prompt_editor.focus_handle(cx),
1283                    ModelUsageContext::InlineAssistant,
1284                    window,
1285                    cx,
1286                )
1287            }),
1288            edited_since_done: false,
1289            prompt_history,
1290            prompt_history_ix: None,
1291            pending_prompt: String::new(),
1292            _codegen_subscription: codegen_subscription,
1293            editor_subscriptions: Vec::new(),
1294            show_rate_limit_notice: false,
1295            mode,
1296            session_state: SessionState {
1297                session_id,
1298                completion: CompletionState::Pending,
1299            },
1300            _phantom: Default::default(),
1301        };
1302
1303        this.assign_completion_provider(cx);
1304        this.subscribe_to_editor(window, cx);
1305        this
1306    }
1307
1308    fn handle_codegen_changed(
1309        &mut self,
1310        codegen: Entity<BufferCodegen>,
1311        cx: &mut Context<PromptEditor<BufferCodegen>>,
1312    ) {
1313        match self.codegen_status(cx) {
1314            CodegenStatus::Idle => {
1315                self.editor
1316                    .update(cx, |editor, _| editor.set_read_only(false));
1317            }
1318            CodegenStatus::Pending => {
1319                self.session_state.completion = CompletionState::Pending;
1320                self.editor
1321                    .update(cx, |editor, _| editor.set_read_only(true));
1322            }
1323            CodegenStatus::Done => {
1324                let completion = codegen.read(cx).active_completion(cx);
1325                self.session_state.completion = CompletionState::Generated {
1326                    completion_text: completion,
1327                };
1328                self.edited_since_done = false;
1329                self.editor
1330                    .update(cx, |editor, _| editor.set_read_only(false));
1331            }
1332            CodegenStatus::Error(_error) => {
1333                self.edited_since_done = false;
1334                self.editor
1335                    .update(cx, |editor, _| editor.set_read_only(false));
1336            }
1337        }
1338    }
1339
1340    pub fn id(&self) -> InlineAssistId {
1341        match &self.mode {
1342            PromptEditorMode::Buffer { id, .. } => *id,
1343            PromptEditorMode::Terminal { .. } => unreachable!(),
1344        }
1345    }
1346
1347    pub fn codegen(&self) -> &Entity<BufferCodegen> {
1348        match &self.mode {
1349            PromptEditorMode::Buffer { codegen, .. } => codegen,
1350            PromptEditorMode::Terminal { .. } => unreachable!(),
1351        }
1352    }
1353
1354    pub fn mention_set(&self) -> &Entity<MentionSet> {
1355        &self.mention_set
1356    }
1357
1358    pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
1359        match &self.mode {
1360            PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
1361            PromptEditorMode::Terminal { .. } => unreachable!(),
1362        }
1363    }
1364}
1365
1366#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1367pub struct TerminalInlineAssistId(pub usize);
1368
1369impl TerminalInlineAssistId {
1370    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
1371        let id = *self;
1372        self.0 += 1;
1373        id
1374    }
1375}
1376
1377impl PromptEditor<TerminalCodegen> {
1378    pub fn new_terminal(
1379        id: TerminalInlineAssistId,
1380        prompt_history: VecDeque<String>,
1381        prompt_buffer: Entity<MultiBuffer>,
1382        codegen: Entity<TerminalCodegen>,
1383        session_id: Uuid,
1384        fs: Arc<dyn Fs>,
1385        thread_store: Entity<ThreadStore>,
1386        prompt_store: Option<Entity<PromptStore>>,
1387        history: WeakEntity<ThreadHistory>,
1388        project: WeakEntity<Project>,
1389        workspace: WeakEntity<Workspace>,
1390        window: &mut Window,
1391        cx: &mut Context<Self>,
1392    ) -> Self {
1393        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1394        let mode = PromptEditorMode::Terminal {
1395            id,
1396            codegen,
1397            height_in_lines: 1,
1398        };
1399
1400        let prompt_editor = cx.new(|cx| {
1401            let mut editor = Editor::new(
1402                EditorMode::AutoHeight {
1403                    min_lines: 1,
1404                    max_lines: Some(Self::MAX_LINES as usize),
1405                },
1406                prompt_buffer,
1407                None,
1408                window,
1409                cx,
1410            );
1411            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1412            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1413            editor.set_context_menu_options(ContextMenuOptions {
1414                min_entries_visible: 12,
1415                max_entries_visible: 12,
1416                placement: None,
1417            });
1418            editor
1419        });
1420
1421        let mention_set = cx
1422            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1423
1424        let model_selector_menu_handle = PopoverMenuHandle::default();
1425
1426        let mut this = Self {
1427            editor: prompt_editor.clone(),
1428            mention_set,
1429            history,
1430            prompt_store,
1431            workspace,
1432            model_selector: cx.new(|cx| {
1433                AgentModelSelector::new(
1434                    fs,
1435                    model_selector_menu_handle.clone(),
1436                    prompt_editor.focus_handle(cx),
1437                    ModelUsageContext::InlineAssistant,
1438                    window,
1439                    cx,
1440                )
1441            }),
1442            edited_since_done: false,
1443            prompt_history,
1444            prompt_history_ix: None,
1445            pending_prompt: String::new(),
1446            _codegen_subscription: codegen_subscription,
1447            editor_subscriptions: Vec::new(),
1448            mode,
1449            show_rate_limit_notice: false,
1450            session_state: SessionState {
1451                session_id,
1452                completion: CompletionState::Pending,
1453            },
1454            _phantom: Default::default(),
1455        };
1456        this.count_lines(cx);
1457        this.assign_completion_provider(cx);
1458        this.subscribe_to_editor(window, cx);
1459        this
1460    }
1461
1462    fn count_lines(&mut self, cx: &mut Context<Self>) {
1463        let height_in_lines = cmp::max(
1464            2, // Make the editor at least two lines tall, to account for padding and buttons.
1465            cmp::min(
1466                self.editor
1467                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1468                Self::MAX_LINES as u32,
1469            ),
1470        ) as u8;
1471
1472        match &mut self.mode {
1473            PromptEditorMode::Terminal {
1474                height_in_lines: current_height,
1475                ..
1476            } => {
1477                if height_in_lines != *current_height {
1478                    *current_height = height_in_lines;
1479                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1480                }
1481            }
1482            PromptEditorMode::Buffer { .. } => unreachable!(),
1483        }
1484    }
1485
1486    fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1487        match &self.codegen().read(cx).status {
1488            CodegenStatus::Idle => {
1489                self.editor
1490                    .update(cx, |editor, _| editor.set_read_only(false));
1491            }
1492            CodegenStatus::Pending => {
1493                self.session_state.completion = CompletionState::Pending;
1494                self.editor
1495                    .update(cx, |editor, _| editor.set_read_only(true));
1496            }
1497            CodegenStatus::Done | CodegenStatus::Error(_) => {
1498                self.session_state.completion = CompletionState::Generated {
1499                    completion_text: codegen.read(cx).completion(),
1500                };
1501                self.edited_since_done = false;
1502                self.editor
1503                    .update(cx, |editor, _| editor.set_read_only(false));
1504            }
1505        }
1506    }
1507
1508    pub fn mention_set(&self) -> &Entity<MentionSet> {
1509        &self.mention_set
1510    }
1511
1512    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1513        match &self.mode {
1514            PromptEditorMode::Buffer { .. } => unreachable!(),
1515            PromptEditorMode::Terminal { codegen, .. } => codegen,
1516        }
1517    }
1518
1519    pub fn id(&self) -> TerminalInlineAssistId {
1520        match &self.mode {
1521            PromptEditorMode::Buffer { .. } => unreachable!(),
1522            PromptEditorMode::Terminal { id, .. } => *id,
1523        }
1524    }
1525}
1526
1527pub enum CodegenStatus {
1528    Idle,
1529    Pending,
1530    Done,
1531    Error(anyhow::Error),
1532}
1533
1534/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1535#[derive(Copy, Clone)]
1536pub enum CancelButtonState {
1537    Idle,
1538    Pending,
1539    Done,
1540    Error,
1541}
1542
1543impl Into<CancelButtonState> for &CodegenStatus {
1544    fn into(self) -> CancelButtonState {
1545        match self {
1546            CodegenStatus::Idle => CancelButtonState::Idle,
1547            CodegenStatus::Pending => CancelButtonState::Pending,
1548            CodegenStatus::Done => CancelButtonState::Done,
1549            CodegenStatus::Error(_) => CancelButtonState::Error,
1550        }
1551    }
1552}
1553
1554#[derive(Copy, Clone)]
1555pub enum GenerationMode {
1556    Generate,
1557    Transform,
1558}
1559
1560impl GenerationMode {
1561    fn start_label(self) -> &'static str {
1562        match self {
1563            GenerationMode::Generate => "Generate",
1564            GenerationMode::Transform => "Transform",
1565        }
1566    }
1567    fn tooltip_interrupt(self) -> &'static str {
1568        match self {
1569            GenerationMode::Generate => "Interrupt Generation",
1570            GenerationMode::Transform => "Interrupt Transform",
1571        }
1572    }
1573
1574    fn tooltip_restart(self) -> &'static str {
1575        match self {
1576            GenerationMode::Generate => "Restart Generation",
1577            GenerationMode::Transform => "Restart Transform",
1578        }
1579    }
1580
1581    fn tooltip_accept(self) -> &'static str {
1582        match self {
1583            GenerationMode::Generate => "Accept Generation",
1584            GenerationMode::Transform => "Accept Transform",
1585        }
1586    }
1587}
1588
1589/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
1590#[derive(Clone, Debug)]
1591struct MessageCrease {
1592    range: Range<MultiBufferOffset>,
1593    icon_path: SharedString,
1594    label: SharedString,
1595}
1596
1597fn extract_message_creases(
1598    editor: &mut Editor,
1599    mention_set: &Entity<MentionSet>,
1600    window: &mut Window,
1601    cx: &mut Context<'_, Editor>,
1602) -> Vec<MessageCrease> {
1603    let creases = mention_set.read(cx).creases();
1604    let snapshot = editor.snapshot(window, cx);
1605    snapshot
1606        .crease_snapshot
1607        .creases()
1608        .filter(|(id, _)| creases.contains(id))
1609        .filter_map(|(_, crease)| {
1610            let metadata = crease.metadata()?.clone();
1611            Some(MessageCrease {
1612                range: crease.range().to_offset(snapshot.buffer()),
1613                label: metadata.label,
1614                icon_path: metadata.icon_path,
1615            })
1616        })
1617        .collect()
1618}
1619
1620fn insert_message_creases(
1621    editor: &mut Editor,
1622    message_creases: &[MessageCrease],
1623    window: &mut Window,
1624    cx: &mut Context<'_, Editor>,
1625) -> Vec<CreaseId> {
1626    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1627    let creases = message_creases
1628        .iter()
1629        .map(|crease| {
1630            let start = buffer_snapshot.anchor_after(crease.range.start);
1631            let end = buffer_snapshot.anchor_before(crease.range.end);
1632            crease_for_mention(
1633                crease.label.clone(),
1634                crease.icon_path.clone(),
1635                start..end,
1636                cx.weak_entity(),
1637            )
1638        })
1639        .collect::<Vec<_>>();
1640    let ids = editor.insert_creases(creases.clone(), cx);
1641    editor.fold_creases(creases, false, window, cx);
1642    ids
1643}