inline_prompt_editor.rs

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