inline_prompt_editor.rs

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