inline_prompt_editor.rs

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