inline_prompt_editor.rs

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