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                let line_height = font_size * 1.2;
1122
1123                let text_style = TextStyle {
1124                    color: colors.editor_foreground,
1125                    font_family: settings.buffer_font.family.clone(),
1126                    font_features: settings.buffer_font.features.clone(),
1127                    font_size: font_size.into(),
1128                    line_height: line_height.into(),
1129                    ..Default::default()
1130                };
1131
1132                EditorElement::new(
1133                    &self.editor,
1134                    EditorStyle {
1135                        background: colors.editor_background,
1136                        local_player: cx.theme().players().local(),
1137                        syntax: cx.theme().syntax().clone(),
1138                        text: text_style,
1139                        ..Default::default()
1140                    },
1141                )
1142            })
1143            .into_any_element()
1144    }
1145
1146    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1147        MarkdownElement::new(markdown, style)
1148    }
1149}
1150
1151pub enum PromptEditorMode {
1152    Buffer {
1153        id: InlineAssistId,
1154        codegen: Entity<BufferCodegen>,
1155        editor_margins: Arc<Mutex<EditorMargins>>,
1156    },
1157    Terminal {
1158        id: TerminalInlineAssistId,
1159        codegen: Entity<TerminalCodegen>,
1160        height_in_lines: u8,
1161    },
1162}
1163
1164pub enum PromptEditorEvent {
1165    StartRequested,
1166    StopRequested,
1167    ConfirmRequested { execute: bool },
1168    CancelRequested,
1169    Resized { height_in_lines: u8 },
1170}
1171
1172#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1173pub struct InlineAssistId(pub usize);
1174
1175impl InlineAssistId {
1176    pub fn post_inc(&mut self) -> InlineAssistId {
1177        let id = *self;
1178        self.0 += 1;
1179        id
1180    }
1181}
1182
1183struct PromptEditorCompletionProviderDelegate;
1184
1185fn inline_assistant_model_supports_images(cx: &App) -> bool {
1186    LanguageModelRegistry::read_global(cx)
1187        .inline_assistant_model()
1188        .map_or(false, |m| m.model.supports_images())
1189}
1190
1191impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
1192    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
1193        vec![
1194            PromptContextType::File,
1195            PromptContextType::Symbol,
1196            PromptContextType::Thread,
1197            PromptContextType::Fetch,
1198            PromptContextType::Rules,
1199        ]
1200    }
1201
1202    fn supports_images(&self, cx: &App) -> bool {
1203        inline_assistant_model_supports_images(cx)
1204    }
1205
1206    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
1207        Vec::new()
1208    }
1209
1210    fn confirm_command(&self, _cx: &mut App) {}
1211}
1212
1213impl PromptEditor<BufferCodegen> {
1214    pub fn new_buffer(
1215        id: InlineAssistId,
1216        editor_margins: Arc<Mutex<EditorMargins>>,
1217        prompt_history: VecDeque<String>,
1218        prompt_buffer: Entity<MultiBuffer>,
1219        codegen: Entity<BufferCodegen>,
1220        session_id: Uuid,
1221        fs: Arc<dyn Fs>,
1222        thread_store: Entity<ThreadStore>,
1223        prompt_store: Option<Entity<PromptStore>>,
1224        history: WeakEntity<AcpThreadHistory>,
1225        project: WeakEntity<Project>,
1226        workspace: WeakEntity<Workspace>,
1227        window: &mut Window,
1228        cx: &mut Context<PromptEditor<BufferCodegen>>,
1229    ) -> PromptEditor<BufferCodegen> {
1230        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1231        let mode = PromptEditorMode::Buffer {
1232            id,
1233            codegen,
1234            editor_margins,
1235        };
1236
1237        let prompt_editor = cx.new(|cx| {
1238            let mut editor = Editor::new(
1239                EditorMode::AutoHeight {
1240                    min_lines: 1,
1241                    max_lines: Some(Self::MAX_LINES as usize),
1242                },
1243                prompt_buffer,
1244                None,
1245                window,
1246                cx,
1247            );
1248            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1249            // Since the prompt editors for all inline assistants are linked,
1250            // always show the cursor (even when it isn't focused) because
1251            // typing in one will make what you typed appear in all of them.
1252            editor.set_show_cursor_when_unfocused(true, cx);
1253            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1254            editor.set_context_menu_options(ContextMenuOptions {
1255                min_entries_visible: 12,
1256                max_entries_visible: 12,
1257                placement: None,
1258            });
1259
1260            editor
1261        });
1262
1263        let mention_set = cx
1264            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1265
1266        let model_selector_menu_handle = PopoverMenuHandle::default();
1267
1268        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
1269            editor: prompt_editor.clone(),
1270            mention_set,
1271            history,
1272            prompt_store,
1273            workspace,
1274            model_selector: cx.new(|cx| {
1275                AgentModelSelector::new(
1276                    fs,
1277                    model_selector_menu_handle,
1278                    prompt_editor.focus_handle(cx),
1279                    ModelUsageContext::InlineAssistant,
1280                    window,
1281                    cx,
1282                )
1283            }),
1284            edited_since_done: false,
1285            prompt_history,
1286            prompt_history_ix: None,
1287            pending_prompt: String::new(),
1288            _codegen_subscription: codegen_subscription,
1289            editor_subscriptions: Vec::new(),
1290            show_rate_limit_notice: false,
1291            mode,
1292            session_state: SessionState {
1293                session_id,
1294                completion: CompletionState::Pending,
1295            },
1296            _phantom: Default::default(),
1297        };
1298
1299        this.assign_completion_provider(cx);
1300        this.subscribe_to_editor(window, cx);
1301        this
1302    }
1303
1304    fn handle_codegen_changed(
1305        &mut self,
1306        codegen: Entity<BufferCodegen>,
1307        cx: &mut Context<PromptEditor<BufferCodegen>>,
1308    ) {
1309        match self.codegen_status(cx) {
1310            CodegenStatus::Idle => {
1311                self.editor
1312                    .update(cx, |editor, _| editor.set_read_only(false));
1313            }
1314            CodegenStatus::Pending => {
1315                self.session_state.completion = CompletionState::Pending;
1316                self.editor
1317                    .update(cx, |editor, _| editor.set_read_only(true));
1318            }
1319            CodegenStatus::Done => {
1320                let completion = codegen.read(cx).active_completion(cx);
1321                self.session_state.completion = CompletionState::Generated {
1322                    completion_text: completion,
1323                };
1324                self.edited_since_done = false;
1325                self.editor
1326                    .update(cx, |editor, _| editor.set_read_only(false));
1327            }
1328            CodegenStatus::Error(_error) => {
1329                self.edited_since_done = false;
1330                self.editor
1331                    .update(cx, |editor, _| editor.set_read_only(false));
1332            }
1333        }
1334    }
1335
1336    pub fn id(&self) -> InlineAssistId {
1337        match &self.mode {
1338            PromptEditorMode::Buffer { id, .. } => *id,
1339            PromptEditorMode::Terminal { .. } => unreachable!(),
1340        }
1341    }
1342
1343    pub fn codegen(&self) -> &Entity<BufferCodegen> {
1344        match &self.mode {
1345            PromptEditorMode::Buffer { codegen, .. } => codegen,
1346            PromptEditorMode::Terminal { .. } => unreachable!(),
1347        }
1348    }
1349
1350    pub fn mention_set(&self) -> &Entity<MentionSet> {
1351        &self.mention_set
1352    }
1353
1354    pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
1355        match &self.mode {
1356            PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
1357            PromptEditorMode::Terminal { .. } => unreachable!(),
1358        }
1359    }
1360}
1361
1362#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1363pub struct TerminalInlineAssistId(pub usize);
1364
1365impl TerminalInlineAssistId {
1366    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
1367        let id = *self;
1368        self.0 += 1;
1369        id
1370    }
1371}
1372
1373impl PromptEditor<TerminalCodegen> {
1374    pub fn new_terminal(
1375        id: TerminalInlineAssistId,
1376        prompt_history: VecDeque<String>,
1377        prompt_buffer: Entity<MultiBuffer>,
1378        codegen: Entity<TerminalCodegen>,
1379        session_id: Uuid,
1380        fs: Arc<dyn Fs>,
1381        thread_store: Entity<ThreadStore>,
1382        prompt_store: Option<Entity<PromptStore>>,
1383        history: WeakEntity<AcpThreadHistory>,
1384        project: WeakEntity<Project>,
1385        workspace: WeakEntity<Workspace>,
1386        window: &mut Window,
1387        cx: &mut Context<Self>,
1388    ) -> Self {
1389        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1390        let mode = PromptEditorMode::Terminal {
1391            id,
1392            codegen,
1393            height_in_lines: 1,
1394        };
1395
1396        let prompt_editor = cx.new(|cx| {
1397            let mut editor = Editor::new(
1398                EditorMode::AutoHeight {
1399                    min_lines: 1,
1400                    max_lines: Some(Self::MAX_LINES as usize),
1401                },
1402                prompt_buffer,
1403                None,
1404                window,
1405                cx,
1406            );
1407            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1408            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1409            editor.set_context_menu_options(ContextMenuOptions {
1410                min_entries_visible: 12,
1411                max_entries_visible: 12,
1412                placement: None,
1413            });
1414            editor
1415        });
1416
1417        let mention_set = cx
1418            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1419
1420        let model_selector_menu_handle = PopoverMenuHandle::default();
1421
1422        let mut this = Self {
1423            editor: prompt_editor.clone(),
1424            mention_set,
1425            history,
1426            prompt_store,
1427            workspace,
1428            model_selector: cx.new(|cx| {
1429                AgentModelSelector::new(
1430                    fs,
1431                    model_selector_menu_handle.clone(),
1432                    prompt_editor.focus_handle(cx),
1433                    ModelUsageContext::InlineAssistant,
1434                    window,
1435                    cx,
1436                )
1437            }),
1438            edited_since_done: false,
1439            prompt_history,
1440            prompt_history_ix: None,
1441            pending_prompt: String::new(),
1442            _codegen_subscription: codegen_subscription,
1443            editor_subscriptions: Vec::new(),
1444            mode,
1445            show_rate_limit_notice: false,
1446            session_state: SessionState {
1447                session_id,
1448                completion: CompletionState::Pending,
1449            },
1450            _phantom: Default::default(),
1451        };
1452        this.count_lines(cx);
1453        this.assign_completion_provider(cx);
1454        this.subscribe_to_editor(window, cx);
1455        this
1456    }
1457
1458    fn count_lines(&mut self, cx: &mut Context<Self>) {
1459        let height_in_lines = cmp::max(
1460            2, // Make the editor at least two lines tall, to account for padding and buttons.
1461            cmp::min(
1462                self.editor
1463                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1464                Self::MAX_LINES as u32,
1465            ),
1466        ) as u8;
1467
1468        match &mut self.mode {
1469            PromptEditorMode::Terminal {
1470                height_in_lines: current_height,
1471                ..
1472            } => {
1473                if height_in_lines != *current_height {
1474                    *current_height = height_in_lines;
1475                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1476                }
1477            }
1478            PromptEditorMode::Buffer { .. } => unreachable!(),
1479        }
1480    }
1481
1482    fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1483        match &self.codegen().read(cx).status {
1484            CodegenStatus::Idle => {
1485                self.editor
1486                    .update(cx, |editor, _| editor.set_read_only(false));
1487            }
1488            CodegenStatus::Pending => {
1489                self.session_state.completion = CompletionState::Pending;
1490                self.editor
1491                    .update(cx, |editor, _| editor.set_read_only(true));
1492            }
1493            CodegenStatus::Done | CodegenStatus::Error(_) => {
1494                self.session_state.completion = CompletionState::Generated {
1495                    completion_text: codegen.read(cx).completion(),
1496                };
1497                self.edited_since_done = false;
1498                self.editor
1499                    .update(cx, |editor, _| editor.set_read_only(false));
1500            }
1501        }
1502    }
1503
1504    pub fn mention_set(&self) -> &Entity<MentionSet> {
1505        &self.mention_set
1506    }
1507
1508    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1509        match &self.mode {
1510            PromptEditorMode::Buffer { .. } => unreachable!(),
1511            PromptEditorMode::Terminal { codegen, .. } => codegen,
1512        }
1513    }
1514
1515    pub fn id(&self) -> TerminalInlineAssistId {
1516        match &self.mode {
1517            PromptEditorMode::Buffer { .. } => unreachable!(),
1518            PromptEditorMode::Terminal { id, .. } => *id,
1519        }
1520    }
1521}
1522
1523pub enum CodegenStatus {
1524    Idle,
1525    Pending,
1526    Done,
1527    Error(anyhow::Error),
1528}
1529
1530/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1531#[derive(Copy, Clone)]
1532pub enum CancelButtonState {
1533    Idle,
1534    Pending,
1535    Done,
1536    Error,
1537}
1538
1539impl Into<CancelButtonState> for &CodegenStatus {
1540    fn into(self) -> CancelButtonState {
1541        match self {
1542            CodegenStatus::Idle => CancelButtonState::Idle,
1543            CodegenStatus::Pending => CancelButtonState::Pending,
1544            CodegenStatus::Done => CancelButtonState::Done,
1545            CodegenStatus::Error(_) => CancelButtonState::Error,
1546        }
1547    }
1548}
1549
1550#[derive(Copy, Clone)]
1551pub enum GenerationMode {
1552    Generate,
1553    Transform,
1554}
1555
1556impl GenerationMode {
1557    fn start_label(self) -> &'static str {
1558        match self {
1559            GenerationMode::Generate => "Generate",
1560            GenerationMode::Transform => "Transform",
1561        }
1562    }
1563    fn tooltip_interrupt(self) -> &'static str {
1564        match self {
1565            GenerationMode::Generate => "Interrupt Generation",
1566            GenerationMode::Transform => "Interrupt Transform",
1567        }
1568    }
1569
1570    fn tooltip_restart(self) -> &'static str {
1571        match self {
1572            GenerationMode::Generate => "Restart Generation",
1573            GenerationMode::Transform => "Restart Transform",
1574        }
1575    }
1576
1577    fn tooltip_accept(self) -> &'static str {
1578        match self {
1579            GenerationMode::Generate => "Accept Generation",
1580            GenerationMode::Transform => "Accept Transform",
1581        }
1582    }
1583}
1584
1585/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
1586#[derive(Clone, Debug)]
1587struct MessageCrease {
1588    range: Range<MultiBufferOffset>,
1589    icon_path: SharedString,
1590    label: SharedString,
1591}
1592
1593fn extract_message_creases(
1594    editor: &mut Editor,
1595    mention_set: &Entity<MentionSet>,
1596    window: &mut Window,
1597    cx: &mut Context<'_, Editor>,
1598) -> Vec<MessageCrease> {
1599    let creases = mention_set.read(cx).creases();
1600    let snapshot = editor.snapshot(window, cx);
1601    snapshot
1602        .crease_snapshot
1603        .creases()
1604        .filter(|(id, _)| creases.contains(id))
1605        .filter_map(|(_, crease)| {
1606            let metadata = crease.metadata()?.clone();
1607            Some(MessageCrease {
1608                range: crease.range().to_offset(snapshot.buffer()),
1609                label: metadata.label,
1610                icon_path: metadata.icon_path,
1611            })
1612        })
1613        .collect()
1614}
1615
1616fn insert_message_creases(
1617    editor: &mut Editor,
1618    message_creases: &[MessageCrease],
1619    window: &mut Window,
1620    cx: &mut Context<'_, Editor>,
1621) -> Vec<CreaseId> {
1622    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1623    let creases = message_creases
1624        .iter()
1625        .map(|crease| {
1626            let start = buffer_snapshot.anchor_after(crease.range.start);
1627            let end = buffer_snapshot.anchor_before(crease.range.end);
1628            crease_for_mention(
1629                crease.label.clone(),
1630                crease.icon_path.clone(),
1631                start..end,
1632                cx.weak_entity(),
1633            )
1634        })
1635        .collect::<Vec<_>>();
1636    let ids = editor.insert_creases(creases.clone(), cx);
1637    editor.fold_creases(creases, false, window, cx);
1638    ids
1639}