inline_prompt_editor.rs

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