inline_prompt_editor.rs

   1use crate::ThreadHistory;
   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_settings::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: Option<WeakEntity<ThreadHistory>>,
  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) = paste_images_as_context(
 421                self.editor.clone(),
 422                self.mention_set.clone(),
 423                self.workspace.clone(),
 424                window,
 425                cx,
 426            )
 427        {
 428            task.detach();
 429        }
 430    }
 431
 432    fn handle_prompt_editor_events(
 433        &mut self,
 434        editor: &Entity<Editor>,
 435        event: &EditorEvent,
 436        window: &mut Window,
 437        cx: &mut Context<Self>,
 438    ) {
 439        match event {
 440            EditorEvent::Edited { .. } => {
 441                let snapshot = editor.update(cx, |editor, cx| editor.snapshot(window, cx));
 442
 443                self.mention_set
 444                    .update(cx, |mention_set, _cx| mention_set.remove_invalid(&snapshot));
 445
 446                if let Some(workspace) = Workspace::for_window(window, cx) {
 447                    workspace.update(cx, |workspace, cx| {
 448                        let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
 449
 450                        workspace
 451                            .client()
 452                            .telemetry()
 453                            .log_edit_event("inline assist", is_via_ssh);
 454                    });
 455                }
 456                let prompt = snapshot.text();
 457                if self
 458                    .prompt_history_ix
 459                    .is_none_or(|ix| self.prompt_history[ix] != prompt)
 460                {
 461                    self.prompt_history_ix.take();
 462                    self.pending_prompt = prompt;
 463                }
 464
 465                self.edited_since_done = true;
 466                self.session_state.completion = CompletionState::Pending;
 467                cx.notify();
 468            }
 469            EditorEvent::Blurred => {
 470                if self.show_rate_limit_notice {
 471                    self.show_rate_limit_notice = false;
 472                    cx.notify();
 473                }
 474            }
 475            _ => {}
 476        }
 477    }
 478
 479    pub fn is_completions_menu_visible(&self, cx: &App) -> bool {
 480        self.editor
 481            .read(cx)
 482            .context_menu()
 483            .borrow()
 484            .as_ref()
 485            .is_some_and(|menu| matches!(menu, CodeContextMenu::Completions(_)) && menu.visible())
 486    }
 487
 488    pub fn trigger_completion_menu(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 489        self.editor.update(cx, |editor, cx| {
 490            let menu_is_open = editor.context_menu().borrow().as_ref().is_some_and(|menu| {
 491                matches!(menu, CodeContextMenu::Completions(_)) && menu.visible()
 492            });
 493
 494            let has_at_sign = {
 495                let snapshot = editor.display_snapshot(cx);
 496                let cursor = editor.selections.newest::<text::Point>(&snapshot).head();
 497                let offset = cursor.to_offset(&snapshot);
 498                if offset.0 > 0 {
 499                    snapshot
 500                        .buffer_snapshot()
 501                        .reversed_chars_at(offset)
 502                        .next()
 503                        .map(|sign| sign == '@')
 504                        .unwrap_or(false)
 505                } else {
 506                    false
 507                }
 508            };
 509
 510            if menu_is_open && has_at_sign {
 511                return;
 512            }
 513
 514            editor.insert("@", window, cx);
 515            editor.show_completions(&editor::actions::ShowCompletions, window, cx);
 516        });
 517    }
 518
 519    fn cancel(
 520        &mut self,
 521        _: &editor::actions::Cancel,
 522        _window: &mut Window,
 523        cx: &mut Context<Self>,
 524    ) {
 525        match self.codegen_status(cx) {
 526            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 527                cx.emit(PromptEditorEvent::CancelRequested);
 528            }
 529            CodegenStatus::Pending => {
 530                cx.emit(PromptEditorEvent::StopRequested);
 531            }
 532        }
 533    }
 534
 535    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 536        match self.codegen_status(cx) {
 537            CodegenStatus::Idle => {
 538                self.fire_started_telemetry(cx);
 539                cx.emit(PromptEditorEvent::StartRequested);
 540            }
 541            CodegenStatus::Pending => {}
 542            CodegenStatus::Done => {
 543                if self.edited_since_done {
 544                    self.fire_started_telemetry(cx);
 545                    cx.emit(PromptEditorEvent::StartRequested);
 546                } else {
 547                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 548                }
 549            }
 550            CodegenStatus::Error(_) => {
 551                self.fire_started_telemetry(cx);
 552                cx.emit(PromptEditorEvent::StartRequested);
 553            }
 554        }
 555    }
 556
 557    fn fire_started_telemetry(&self, cx: &Context<Self>) {
 558        let Some(model) = LanguageModelRegistry::read_global(cx).inline_assistant_model() else {
 559            return;
 560        };
 561
 562        let model_telemetry_id = model.model.telemetry_id();
 563        let model_provider_id = model.provider.id().to_string();
 564
 565        let (kind, language_name) = match &self.mode {
 566            PromptEditorMode::Buffer { codegen, .. } => {
 567                let codegen = codegen.read(cx);
 568                (
 569                    "inline",
 570                    codegen.language_name(cx).map(|name| name.to_string()),
 571                )
 572            }
 573            PromptEditorMode::Terminal { .. } => ("inline_terminal", None),
 574        };
 575
 576        telemetry::event!(
 577            "Assistant Started",
 578            session_id = self.session_state.session_id.to_string(),
 579            kind = kind,
 580            phase = "started",
 581            model = model_telemetry_id,
 582            model_provider = model_provider_id,
 583            language_name = language_name,
 584        );
 585    }
 586
 587    fn thumbs_up(&mut self, _: &ThumbsUpResult, _window: &mut Window, cx: &mut Context<Self>) {
 588        match &self.session_state.completion {
 589            CompletionState::Pending => {
 590                self.toast("Can't rate, still generating...", None, cx);
 591                return;
 592            }
 593            CompletionState::Rated => {
 594                self.toast(
 595                    "Already rated this completion",
 596                    Some(self.session_state.session_id),
 597                    cx,
 598                );
 599                return;
 600            }
 601            CompletionState::Generated { completion_text } => {
 602                let model_info = self.model_selector.read(cx).active_model(cx);
 603                let (model_id, use_streaming_tools) = {
 604                    let Some(configured_model) = model_info else {
 605                        self.toast("No configured model", None, cx);
 606                        return;
 607                    };
 608                    (
 609                        configured_model.model.telemetry_id(),
 610                        CodegenAlternative::use_streaming_tools(
 611                            configured_model.model.as_ref(),
 612                            cx,
 613                        ),
 614                    )
 615                };
 616
 617                let selected_text = match &self.mode {
 618                    PromptEditorMode::Buffer { codegen, .. } => {
 619                        codegen.read(cx).selected_text(cx).map(|s| s.to_string())
 620                    }
 621                    PromptEditorMode::Terminal { .. } => None,
 622                };
 623
 624                let prompt = self.editor.read(cx).text(cx);
 625
 626                let kind = match &self.mode {
 627                    PromptEditorMode::Buffer { .. } => "inline",
 628                    PromptEditorMode::Terminal { .. } => "inline_terminal",
 629                };
 630
 631                telemetry::event!(
 632                    "Inline Assistant Rated",
 633                    rating = "positive",
 634                    session_id = self.session_state.session_id.to_string(),
 635                    kind = kind,
 636                    model = model_id,
 637                    prompt = prompt,
 638                    completion = completion_text,
 639                    selected_text = selected_text,
 640                    use_streaming_tools
 641                );
 642
 643                self.session_state.completion = CompletionState::Rated;
 644
 645                cx.notify();
 646            }
 647        }
 648    }
 649
 650    fn thumbs_down(&mut self, _: &ThumbsDownResult, _window: &mut Window, cx: &mut Context<Self>) {
 651        match &self.session_state.completion {
 652            CompletionState::Pending => {
 653                self.toast("Can't rate, still generating...", None, cx);
 654                return;
 655            }
 656            CompletionState::Rated => {
 657                self.toast(
 658                    "Already rated this completion",
 659                    Some(self.session_state.session_id),
 660                    cx,
 661                );
 662                return;
 663            }
 664            CompletionState::Generated { completion_text } => {
 665                let model_info = self.model_selector.read(cx).active_model(cx);
 666                let (model_telemetry_id, use_streaming_tools) = {
 667                    let Some(configured_model) = model_info else {
 668                        self.toast("No configured model", None, cx);
 669                        return;
 670                    };
 671                    (
 672                        configured_model.model.telemetry_id(),
 673                        CodegenAlternative::use_streaming_tools(
 674                            configured_model.model.as_ref(),
 675                            cx,
 676                        ),
 677                    )
 678                };
 679
 680                let selected_text = match &self.mode {
 681                    PromptEditorMode::Buffer { codegen, .. } => {
 682                        codegen.read(cx).selected_text(cx).map(|s| s.to_string())
 683                    }
 684                    PromptEditorMode::Terminal { .. } => None,
 685                };
 686
 687                let prompt = self.editor.read(cx).text(cx);
 688
 689                let kind = match &self.mode {
 690                    PromptEditorMode::Buffer { .. } => "inline",
 691                    PromptEditorMode::Terminal { .. } => "inline_terminal",
 692                };
 693
 694                telemetry::event!(
 695                    "Inline Assistant Rated",
 696                    rating = "negative",
 697                    session_id = self.session_state.session_id.to_string(),
 698                    kind = kind,
 699                    model = model_telemetry_id,
 700                    prompt = prompt,
 701                    completion = completion_text,
 702                    selected_text = selected_text,
 703                    use_streaming_tools
 704                );
 705
 706                self.session_state.completion = CompletionState::Rated;
 707
 708                cx.notify();
 709            }
 710        }
 711    }
 712
 713    fn toast(&mut self, msg: &str, uuid: Option<Uuid>, cx: &mut Context<'_, PromptEditor<T>>) {
 714        self.workspace
 715            .update(cx, |workspace, cx| {
 716                enum InlinePromptRating {}
 717                workspace.show_toast(
 718                    {
 719                        let mut toast = Toast::new(
 720                            NotificationId::unique::<InlinePromptRating>(),
 721                            msg.to_string(),
 722                        )
 723                        .autohide();
 724
 725                        if let Some(uuid) = uuid {
 726                            toast = toast.on_click("Click to copy rating ID", move |_, cx| {
 727                                cx.write_to_clipboard(ClipboardItem::new_string(uuid.to_string()));
 728                            });
 729                        };
 730
 731                        toast
 732                    },
 733                    cx,
 734                );
 735            })
 736            .ok();
 737    }
 738
 739    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 740        if let Some(ix) = self.prompt_history_ix {
 741            if ix > 0 {
 742                self.prompt_history_ix = Some(ix - 1);
 743                let prompt = self.prompt_history[ix - 1].as_str();
 744                self.editor.update(cx, |editor, cx| {
 745                    editor.set_text(prompt, window, cx);
 746                    editor.move_to_beginning(&Default::default(), window, cx);
 747                });
 748            }
 749        } else if !self.prompt_history.is_empty() {
 750            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 751            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 752            self.editor.update(cx, |editor, cx| {
 753                editor.set_text(prompt, window, cx);
 754                editor.move_to_beginning(&Default::default(), window, cx);
 755            });
 756        }
 757    }
 758
 759    fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
 760        if let Some(ix) = self.prompt_history_ix {
 761            if ix < self.prompt_history.len() - 1 {
 762                self.prompt_history_ix = Some(ix + 1);
 763                let prompt = self.prompt_history[ix + 1].as_str();
 764                self.editor.update(cx, |editor, cx| {
 765                    editor.set_text(prompt, window, cx);
 766                    editor.move_to_end(&Default::default(), window, cx)
 767                });
 768            } else {
 769                self.prompt_history_ix = None;
 770                let prompt = self.pending_prompt.as_str();
 771                self.editor.update(cx, |editor, cx| {
 772                    editor.set_text(prompt, window, cx);
 773                    editor.move_to_end(&Default::default(), window, cx)
 774                });
 775            }
 776        }
 777    }
 778
 779    fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
 780        let mode = match &self.mode {
 781            PromptEditorMode::Buffer { codegen, .. } => {
 782                let codegen = codegen.read(cx);
 783                if codegen.is_insertion {
 784                    GenerationMode::Generate
 785                } else {
 786                    GenerationMode::Transform
 787                }
 788            }
 789            PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
 790        };
 791
 792        let codegen_status = self.codegen_status(cx);
 793
 794        match codegen_status {
 795            CodegenStatus::Idle => {
 796                vec![
 797                    Button::new("start", mode.start_label())
 798                        .label_size(LabelSize::Small)
 799                        .end_icon(
 800                            Icon::new(IconName::Return)
 801                                .size(IconSize::XSmall)
 802                                .color(Color::Muted),
 803                        )
 804                        .on_click(
 805                            cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
 806                        )
 807                        .into_any_element(),
 808                ]
 809            }
 810            CodegenStatus::Pending => vec![
 811                IconButton::new("stop", IconName::Stop)
 812                    .icon_color(Color::Error)
 813                    .shape(IconButtonShape::Square)
 814                    .tooltip(move |_window, cx| {
 815                        Tooltip::with_meta(
 816                            mode.tooltip_interrupt(),
 817                            Some(&menu::Cancel),
 818                            "Changes won't be discarded",
 819                            cx,
 820                        )
 821                    })
 822                    .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 823                    .into_any_element(),
 824            ],
 825            CodegenStatus::Done | CodegenStatus::Error(_) => {
 826                let has_error = matches!(codegen_status, CodegenStatus::Error(_));
 827                if has_error || self.edited_since_done {
 828                    vec![
 829                        IconButton::new("restart", IconName::RotateCw)
 830                            .icon_color(Color::Info)
 831                            .shape(IconButtonShape::Square)
 832                            .tooltip(move |_window, cx| {
 833                                Tooltip::with_meta(
 834                                    mode.tooltip_restart(),
 835                                    Some(&menu::Confirm),
 836                                    "Changes will be discarded",
 837                                    cx,
 838                                )
 839                            })
 840                            .on_click(cx.listener(|_, _, _, cx| {
 841                                cx.emit(PromptEditorEvent::StartRequested);
 842                            }))
 843                            .into_any_element(),
 844                    ]
 845                } else {
 846                    let rated = matches!(self.session_state.completion, CompletionState::Rated);
 847
 848                    let accept = IconButton::new("accept", IconName::Check)
 849                        .icon_color(Color::Info)
 850                        .shape(IconButtonShape::Square)
 851                        .tooltip(move |_window, cx| {
 852                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, cx)
 853                        })
 854                        .on_click(cx.listener(|_, _, _, cx| {
 855                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 856                        }))
 857                        .into_any_element();
 858
 859                    let mut buttons = Vec::new();
 860
 861                    if AgentSettings::get_global(cx).enable_feedback {
 862                        buttons.push(
 863                            h_flex()
 864                                .pl_1()
 865                                .gap_1()
 866                                .border_l_1()
 867                                .border_color(cx.theme().colors().border_variant)
 868                                .child(
 869                                    IconButton::new("thumbs-up", IconName::ThumbsUp)
 870                                        .shape(IconButtonShape::Square)
 871                                        .map(|this| {
 872                                            if rated {
 873                                                this.disabled(true)
 874                                                    .icon_color(Color::Disabled)
 875                                                    .tooltip(move |_, cx| {
 876                                                        Tooltip::with_meta(
 877                                                            "Good Result",
 878                                                            None,
 879                                                            "You already rated this result",
 880                                                            cx,
 881                                                        )
 882                                                    })
 883                                            } else {
 884                                                this.icon_color(Color::Muted).tooltip(
 885                                                    move |_, cx| {
 886                                                        Tooltip::for_action(
 887                                                            "Good Result",
 888                                                            &ThumbsUpResult,
 889                                                            cx,
 890                                                        )
 891                                                    },
 892                                                )
 893                                            }
 894                                        })
 895                                        .on_click(cx.listener(|this, _, window, cx| {
 896                                            this.thumbs_up(&ThumbsUpResult, window, cx);
 897                                        })),
 898                                )
 899                                .child(
 900                                    IconButton::new("thumbs-down", IconName::ThumbsDown)
 901                                        .shape(IconButtonShape::Square)
 902                                        .map(|this| {
 903                                            if rated {
 904                                                this.disabled(true)
 905                                                    .icon_color(Color::Disabled)
 906                                                    .tooltip(move |_, cx| {
 907                                                        Tooltip::with_meta(
 908                                                            "Bad Result",
 909                                                            None,
 910                                                            "You already rated this result",
 911                                                            cx,
 912                                                        )
 913                                                    })
 914                                            } else {
 915                                                this.icon_color(Color::Muted).tooltip(
 916                                                    move |_, cx| {
 917                                                        Tooltip::for_action(
 918                                                            "Bad Result",
 919                                                            &ThumbsDownResult,
 920                                                            cx,
 921                                                        )
 922                                                    },
 923                                                )
 924                                            }
 925                                        })
 926                                        .on_click(cx.listener(|this, _, window, cx| {
 927                                            this.thumbs_down(&ThumbsDownResult, window, cx);
 928                                        })),
 929                                )
 930                                .into_any_element(),
 931                        );
 932                    }
 933
 934                    buttons.push(accept);
 935
 936                    match &self.mode {
 937                        PromptEditorMode::Terminal { .. } => {
 938                            buttons.push(
 939                                IconButton::new("confirm", IconName::PlayFilled)
 940                                    .icon_color(Color::Info)
 941                                    .shape(IconButtonShape::Square)
 942                                    .tooltip(|_window, cx| {
 943                                        Tooltip::for_action(
 944                                            "Execute Generated Command",
 945                                            &menu::SecondaryConfirm,
 946                                            cx,
 947                                        )
 948                                    })
 949                                    .on_click(cx.listener(|_, _, _, cx| {
 950                                        cx.emit(PromptEditorEvent::ConfirmRequested {
 951                                            execute: true,
 952                                        });
 953                                    }))
 954                                    .into_any_element(),
 955                            );
 956                            buttons
 957                        }
 958                        PromptEditorMode::Buffer { .. } => buttons,
 959                    }
 960                }
 961            }
 962        }
 963    }
 964
 965    fn cycle_prev(
 966        &mut self,
 967        _: &CyclePreviousInlineAssist,
 968        _: &mut Window,
 969        cx: &mut Context<Self>,
 970    ) {
 971        match &self.mode {
 972            PromptEditorMode::Buffer { codegen, .. } => {
 973                codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
 974            }
 975            PromptEditorMode::Terminal { .. } => {
 976                // no cycle buttons in terminal mode
 977            }
 978        }
 979    }
 980
 981    fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
 982        match &self.mode {
 983            PromptEditorMode::Buffer { codegen, .. } => {
 984                codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
 985            }
 986            PromptEditorMode::Terminal { .. } => {
 987                // no cycle buttons in terminal mode
 988            }
 989        }
 990    }
 991
 992    fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
 993        let focus_handle = self.editor.focus_handle(cx);
 994
 995        IconButton::new("cancel", IconName::Close)
 996            .icon_color(Color::Muted)
 997            .shape(IconButtonShape::Square)
 998            .tooltip({
 999                move |_window, cx| {
1000                    Tooltip::for_action_in(
1001                        "Close Assistant",
1002                        &editor::actions::Cancel,
1003                        &focus_handle,
1004                        cx,
1005                    )
1006                }
1007            })
1008            .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
1009            .into_any_element()
1010    }
1011
1012    fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
1013        let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
1014
1015        let model_registry = LanguageModelRegistry::read_global(cx);
1016        let default_model = model_registry.default_model().map(|default| default.model);
1017        let alternative_models = model_registry.inline_alternative_models();
1018
1019        let get_model_name = |index: usize| -> String {
1020            let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
1021
1022            match index {
1023                0 => default_model.as_ref().map_or_else(String::new, name),
1024                index if index <= alternative_models.len() => alternative_models
1025                    .get(index - 1)
1026                    .map_or_else(String::new, name),
1027                _ => String::new(),
1028            }
1029        };
1030
1031        let total_models = alternative_models.len() + 1;
1032
1033        if total_models <= 1 {
1034            return div().into_any_element();
1035        }
1036
1037        let current_index = codegen.active_alternative;
1038        let prev_index = (current_index + total_models - 1) % total_models;
1039        let next_index = (current_index + 1) % total_models;
1040
1041        let prev_model_name = get_model_name(prev_index);
1042        let next_model_name = get_model_name(next_index);
1043
1044        h_flex()
1045            .child(
1046                IconButton::new("previous", IconName::ChevronLeft)
1047                    .icon_color(Color::Muted)
1048                    .disabled(disabled || current_index == 0)
1049                    .shape(IconButtonShape::Square)
1050                    .tooltip({
1051                        let focus_handle = self.editor.focus_handle(cx);
1052                        move |_window, cx| {
1053                            cx.new(|cx| {
1054                                let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
1055                                    KeyBinding::for_action_in(
1056                                        &CyclePreviousInlineAssist,
1057                                        &focus_handle,
1058                                        cx,
1059                                    ),
1060                                );
1061                                if !disabled && current_index != 0 {
1062                                    tooltip = tooltip.meta(prev_model_name.clone());
1063                                }
1064                                tooltip
1065                            })
1066                            .into()
1067                        }
1068                    })
1069                    .on_click(cx.listener(|this, _, window, cx| {
1070                        this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
1071                    })),
1072            )
1073            .child(
1074                Label::new(format!(
1075                    "{}/{}",
1076                    codegen.active_alternative + 1,
1077                    codegen.alternative_count(cx)
1078                ))
1079                .size(LabelSize::Small)
1080                .color(if disabled {
1081                    Color::Disabled
1082                } else {
1083                    Color::Muted
1084                }),
1085            )
1086            .child(
1087                IconButton::new("next", IconName::ChevronRight)
1088                    .icon_color(Color::Muted)
1089                    .disabled(disabled || current_index == total_models - 1)
1090                    .shape(IconButtonShape::Square)
1091                    .tooltip({
1092                        let focus_handle = self.editor.focus_handle(cx);
1093                        move |_window, cx| {
1094                            cx.new(|cx| {
1095                                let mut tooltip = Tooltip::new("Next Alternative").key_binding(
1096                                    KeyBinding::for_action_in(
1097                                        &CycleNextInlineAssist,
1098                                        &focus_handle,
1099                                        cx,
1100                                    ),
1101                                );
1102                                if !disabled && current_index != total_models - 1 {
1103                                    tooltip = tooltip.meta(next_model_name.clone());
1104                                }
1105                                tooltip
1106                            })
1107                            .into()
1108                        }
1109                    })
1110                    .on_click(cx.listener(|this, _, window, cx| {
1111                        this.cycle_next(&CycleNextInlineAssist, window, cx)
1112                    })),
1113            )
1114            .into_any_element()
1115    }
1116
1117    fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
1118        let colors = cx.theme().colors();
1119
1120        div()
1121            .size_full()
1122            .p_2()
1123            .pl_1()
1124            .bg(colors.editor_background)
1125            .child({
1126                let settings = ThemeSettings::get_global(cx);
1127                let font_size = settings.buffer_font_size(cx);
1128
1129                let text_style = TextStyle {
1130                    color: colors.editor_foreground,
1131                    font_family: settings.buffer_font.family.clone(),
1132                    font_features: settings.buffer_font.features.clone(),
1133                    font_size: font_size.into(),
1134                    line_height: relative(settings.buffer_line_height.value()),
1135                    ..Default::default()
1136                };
1137
1138                EditorElement::new(
1139                    &self.editor,
1140                    EditorStyle {
1141                        background: colors.editor_background,
1142                        local_player: cx.theme().players().local(),
1143                        syntax: cx.theme().syntax().clone(),
1144                        text: text_style,
1145                        ..Default::default()
1146                    },
1147                )
1148            })
1149            .into_any_element()
1150    }
1151
1152    fn render_markdown(&self, markdown: Entity<Markdown>, style: MarkdownStyle) -> MarkdownElement {
1153        MarkdownElement::new(markdown, style)
1154    }
1155}
1156
1157pub enum PromptEditorMode {
1158    Buffer {
1159        id: InlineAssistId,
1160        codegen: Entity<BufferCodegen>,
1161        editor_margins: Arc<Mutex<EditorMargins>>,
1162    },
1163    Terminal {
1164        id: TerminalInlineAssistId,
1165        codegen: Entity<TerminalCodegen>,
1166        height_in_lines: u8,
1167    },
1168}
1169
1170pub enum PromptEditorEvent {
1171    StartRequested,
1172    StopRequested,
1173    ConfirmRequested { execute: bool },
1174    CancelRequested,
1175    Resized { height_in_lines: u8 },
1176}
1177
1178#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1179pub struct InlineAssistId(pub usize);
1180
1181impl InlineAssistId {
1182    pub fn post_inc(&mut self) -> InlineAssistId {
1183        let id = *self;
1184        self.0 += 1;
1185        id
1186    }
1187}
1188
1189struct PromptEditorCompletionProviderDelegate;
1190
1191fn inline_assistant_model_supports_images(cx: &App) -> bool {
1192    LanguageModelRegistry::read_global(cx)
1193        .inline_assistant_model()
1194        .map_or(false, |m| m.model.supports_images())
1195}
1196
1197impl PromptCompletionProviderDelegate for PromptEditorCompletionProviderDelegate {
1198    fn supported_modes(&self, _cx: &App) -> Vec<PromptContextType> {
1199        vec![
1200            PromptContextType::File,
1201            PromptContextType::Symbol,
1202            PromptContextType::Thread,
1203            PromptContextType::Fetch,
1204            PromptContextType::Rules,
1205        ]
1206    }
1207
1208    fn supports_images(&self, cx: &App) -> bool {
1209        inline_assistant_model_supports_images(cx)
1210    }
1211
1212    fn available_commands(&self, _cx: &App) -> Vec<crate::completion_provider::AvailableCommand> {
1213        Vec::new()
1214    }
1215
1216    fn confirm_command(&self, _cx: &mut App) {}
1217}
1218
1219impl PromptEditor<BufferCodegen> {
1220    pub fn new_buffer(
1221        id: InlineAssistId,
1222        editor_margins: Arc<Mutex<EditorMargins>>,
1223        prompt_history: VecDeque<String>,
1224        prompt_buffer: Entity<MultiBuffer>,
1225        codegen: Entity<BufferCodegen>,
1226        session_id: Uuid,
1227        fs: Arc<dyn Fs>,
1228        thread_store: Entity<ThreadStore>,
1229        prompt_store: Option<Entity<PromptStore>>,
1230        history: Option<WeakEntity<ThreadHistory>>,
1231        project: WeakEntity<Project>,
1232        workspace: WeakEntity<Workspace>,
1233        window: &mut Window,
1234        cx: &mut Context<PromptEditor<BufferCodegen>>,
1235    ) -> PromptEditor<BufferCodegen> {
1236        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1237        let mode = PromptEditorMode::Buffer {
1238            id,
1239            codegen,
1240            editor_margins,
1241        };
1242
1243        let prompt_editor = cx.new(|cx| {
1244            let mut editor = Editor::new(
1245                EditorMode::AutoHeight {
1246                    min_lines: 1,
1247                    max_lines: Some(Self::MAX_LINES as usize),
1248                },
1249                prompt_buffer,
1250                None,
1251                window,
1252                cx,
1253            );
1254            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1255            // Since the prompt editors for all inline assistants are linked,
1256            // always show the cursor (even when it isn't focused) because
1257            // typing in one will make what you typed appear in all of them.
1258            editor.set_show_cursor_when_unfocused(true, cx);
1259            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1260            editor.set_context_menu_options(ContextMenuOptions {
1261                min_entries_visible: 12,
1262                max_entries_visible: 12,
1263                placement: None,
1264            });
1265
1266            editor
1267        });
1268
1269        let mention_set = cx
1270            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1271
1272        let model_selector_menu_handle = PopoverMenuHandle::default();
1273
1274        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
1275            editor: prompt_editor.clone(),
1276            mention_set,
1277            history,
1278            prompt_store,
1279            workspace,
1280            model_selector: cx.new(|cx| {
1281                AgentModelSelector::new(
1282                    fs,
1283                    model_selector_menu_handle,
1284                    prompt_editor.focus_handle(cx),
1285                    ModelUsageContext::InlineAssistant,
1286                    window,
1287                    cx,
1288                )
1289            }),
1290            edited_since_done: false,
1291            prompt_history,
1292            prompt_history_ix: None,
1293            pending_prompt: String::new(),
1294            _codegen_subscription: codegen_subscription,
1295            editor_subscriptions: Vec::new(),
1296            show_rate_limit_notice: false,
1297            mode,
1298            session_state: SessionState {
1299                session_id,
1300                completion: CompletionState::Pending,
1301            },
1302            _phantom: Default::default(),
1303        };
1304
1305        this.assign_completion_provider(cx);
1306        this.subscribe_to_editor(window, cx);
1307        this
1308    }
1309
1310    fn handle_codegen_changed(
1311        &mut self,
1312        codegen: Entity<BufferCodegen>,
1313        cx: &mut Context<PromptEditor<BufferCodegen>>,
1314    ) {
1315        match self.codegen_status(cx) {
1316            CodegenStatus::Idle => {
1317                self.editor
1318                    .update(cx, |editor, _| editor.set_read_only(false));
1319            }
1320            CodegenStatus::Pending => {
1321                self.session_state.completion = CompletionState::Pending;
1322                self.editor
1323                    .update(cx, |editor, _| editor.set_read_only(true));
1324            }
1325            CodegenStatus::Done => {
1326                let completion = codegen.read(cx).active_completion(cx);
1327                self.session_state.completion = CompletionState::Generated {
1328                    completion_text: completion,
1329                };
1330                self.edited_since_done = false;
1331                self.editor
1332                    .update(cx, |editor, _| editor.set_read_only(false));
1333            }
1334            CodegenStatus::Error(_error) => {
1335                self.edited_since_done = false;
1336                self.editor
1337                    .update(cx, |editor, _| editor.set_read_only(false));
1338            }
1339        }
1340    }
1341
1342    pub fn id(&self) -> InlineAssistId {
1343        match &self.mode {
1344            PromptEditorMode::Buffer { id, .. } => *id,
1345            PromptEditorMode::Terminal { .. } => unreachable!(),
1346        }
1347    }
1348
1349    pub fn codegen(&self) -> &Entity<BufferCodegen> {
1350        match &self.mode {
1351            PromptEditorMode::Buffer { codegen, .. } => codegen,
1352            PromptEditorMode::Terminal { .. } => unreachable!(),
1353        }
1354    }
1355
1356    pub fn mention_set(&self) -> &Entity<MentionSet> {
1357        &self.mention_set
1358    }
1359
1360    pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
1361        match &self.mode {
1362            PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
1363            PromptEditorMode::Terminal { .. } => unreachable!(),
1364        }
1365    }
1366}
1367
1368#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
1369pub struct TerminalInlineAssistId(pub usize);
1370
1371impl TerminalInlineAssistId {
1372    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
1373        let id = *self;
1374        self.0 += 1;
1375        id
1376    }
1377}
1378
1379impl PromptEditor<TerminalCodegen> {
1380    pub fn new_terminal(
1381        id: TerminalInlineAssistId,
1382        prompt_history: VecDeque<String>,
1383        prompt_buffer: Entity<MultiBuffer>,
1384        codegen: Entity<TerminalCodegen>,
1385        session_id: Uuid,
1386        fs: Arc<dyn Fs>,
1387        thread_store: Entity<ThreadStore>,
1388        prompt_store: Option<Entity<PromptStore>>,
1389        history: Option<WeakEntity<ThreadHistory>>,
1390        project: WeakEntity<Project>,
1391        workspace: WeakEntity<Workspace>,
1392        window: &mut Window,
1393        cx: &mut Context<Self>,
1394    ) -> Self {
1395        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
1396        let mode = PromptEditorMode::Terminal {
1397            id,
1398            codegen,
1399            height_in_lines: 1,
1400        };
1401
1402        let prompt_editor = cx.new(|cx| {
1403            let mut editor = Editor::new(
1404                EditorMode::AutoHeight {
1405                    min_lines: 1,
1406                    max_lines: Some(Self::MAX_LINES as usize),
1407                },
1408                prompt_buffer,
1409                None,
1410                window,
1411                cx,
1412            );
1413            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
1414            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
1415            editor.set_context_menu_options(ContextMenuOptions {
1416                min_entries_visible: 12,
1417                max_entries_visible: 12,
1418                placement: None,
1419            });
1420            editor
1421        });
1422
1423        let mention_set = cx
1424            .new(|_cx| MentionSet::new(project, Some(thread_store.clone()), prompt_store.clone()));
1425
1426        let model_selector_menu_handle = PopoverMenuHandle::default();
1427
1428        let mut this = Self {
1429            editor: prompt_editor.clone(),
1430            mention_set,
1431            history,
1432            prompt_store,
1433            workspace,
1434            model_selector: cx.new(|cx| {
1435                AgentModelSelector::new(
1436                    fs,
1437                    model_selector_menu_handle.clone(),
1438                    prompt_editor.focus_handle(cx),
1439                    ModelUsageContext::InlineAssistant,
1440                    window,
1441                    cx,
1442                )
1443            }),
1444            edited_since_done: false,
1445            prompt_history,
1446            prompt_history_ix: None,
1447            pending_prompt: String::new(),
1448            _codegen_subscription: codegen_subscription,
1449            editor_subscriptions: Vec::new(),
1450            mode,
1451            show_rate_limit_notice: false,
1452            session_state: SessionState {
1453                session_id,
1454                completion: CompletionState::Pending,
1455            },
1456            _phantom: Default::default(),
1457        };
1458        this.count_lines(cx);
1459        this.assign_completion_provider(cx);
1460        this.subscribe_to_editor(window, cx);
1461        this
1462    }
1463
1464    fn count_lines(&mut self, cx: &mut Context<Self>) {
1465        let height_in_lines = cmp::max(
1466            2, // Make the editor at least two lines tall, to account for padding and buttons.
1467            cmp::min(
1468                self.editor
1469                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1470                Self::MAX_LINES as u32,
1471            ),
1472        ) as u8;
1473
1474        match &mut self.mode {
1475            PromptEditorMode::Terminal {
1476                height_in_lines: current_height,
1477                ..
1478            } => {
1479                if height_in_lines != *current_height {
1480                    *current_height = height_in_lines;
1481                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1482                }
1483            }
1484            PromptEditorMode::Buffer { .. } => unreachable!(),
1485        }
1486    }
1487
1488    fn handle_codegen_changed(&mut self, codegen: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1489        match &self.codegen().read(cx).status {
1490            CodegenStatus::Idle => {
1491                self.editor
1492                    .update(cx, |editor, _| editor.set_read_only(false));
1493            }
1494            CodegenStatus::Pending => {
1495                self.session_state.completion = CompletionState::Pending;
1496                self.editor
1497                    .update(cx, |editor, _| editor.set_read_only(true));
1498            }
1499            CodegenStatus::Done | CodegenStatus::Error(_) => {
1500                self.session_state.completion = CompletionState::Generated {
1501                    completion_text: codegen.read(cx).completion(),
1502                };
1503                self.edited_since_done = false;
1504                self.editor
1505                    .update(cx, |editor, _| editor.set_read_only(false));
1506            }
1507        }
1508    }
1509
1510    pub fn mention_set(&self) -> &Entity<MentionSet> {
1511        &self.mention_set
1512    }
1513
1514    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1515        match &self.mode {
1516            PromptEditorMode::Buffer { .. } => unreachable!(),
1517            PromptEditorMode::Terminal { codegen, .. } => codegen,
1518        }
1519    }
1520
1521    pub fn id(&self) -> TerminalInlineAssistId {
1522        match &self.mode {
1523            PromptEditorMode::Buffer { .. } => unreachable!(),
1524            PromptEditorMode::Terminal { id, .. } => *id,
1525        }
1526    }
1527}
1528
1529pub enum CodegenStatus {
1530    Idle,
1531    Pending,
1532    Done,
1533    Error(anyhow::Error),
1534}
1535
1536/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1537#[derive(Copy, Clone)]
1538pub enum CancelButtonState {
1539    Idle,
1540    Pending,
1541    Done,
1542    Error,
1543}
1544
1545impl Into<CancelButtonState> for &CodegenStatus {
1546    fn into(self) -> CancelButtonState {
1547        match self {
1548            CodegenStatus::Idle => CancelButtonState::Idle,
1549            CodegenStatus::Pending => CancelButtonState::Pending,
1550            CodegenStatus::Done => CancelButtonState::Done,
1551            CodegenStatus::Error(_) => CancelButtonState::Error,
1552        }
1553    }
1554}
1555
1556#[derive(Copy, Clone)]
1557pub enum GenerationMode {
1558    Generate,
1559    Transform,
1560}
1561
1562impl GenerationMode {
1563    fn start_label(self) -> &'static str {
1564        match self {
1565            GenerationMode::Generate => "Generate",
1566            GenerationMode::Transform => "Transform",
1567        }
1568    }
1569    fn tooltip_interrupt(self) -> &'static str {
1570        match self {
1571            GenerationMode::Generate => "Interrupt Generation",
1572            GenerationMode::Transform => "Interrupt Transform",
1573        }
1574    }
1575
1576    fn tooltip_restart(self) -> &'static str {
1577        match self {
1578            GenerationMode::Generate => "Restart Generation",
1579            GenerationMode::Transform => "Restart Transform",
1580        }
1581    }
1582
1583    fn tooltip_accept(self) -> &'static str {
1584        match self {
1585            GenerationMode::Generate => "Accept Generation",
1586            GenerationMode::Transform => "Accept Transform",
1587        }
1588    }
1589}
1590
1591/// Stored information that can be used to resurrect a context crease when creating an editor for a past message.
1592#[derive(Clone, Debug)]
1593struct MessageCrease {
1594    range: Range<MultiBufferOffset>,
1595    icon_path: SharedString,
1596    label: SharedString,
1597}
1598
1599fn extract_message_creases(
1600    editor: &mut Editor,
1601    mention_set: &Entity<MentionSet>,
1602    window: &mut Window,
1603    cx: &mut Context<'_, Editor>,
1604) -> Vec<MessageCrease> {
1605    let creases = mention_set.read(cx).creases();
1606    let snapshot = editor.snapshot(window, cx);
1607    snapshot
1608        .crease_snapshot
1609        .creases()
1610        .filter(|(id, _)| creases.contains(id))
1611        .filter_map(|(_, crease)| {
1612            let metadata = crease.metadata()?.clone();
1613            Some(MessageCrease {
1614                range: crease.range().to_offset(snapshot.buffer()),
1615                label: metadata.label,
1616                icon_path: metadata.icon_path,
1617            })
1618        })
1619        .collect()
1620}
1621
1622fn insert_message_creases(
1623    editor: &mut Editor,
1624    message_creases: &[MessageCrease],
1625    window: &mut Window,
1626    cx: &mut Context<'_, Editor>,
1627) -> Vec<CreaseId> {
1628    let buffer_snapshot = editor.buffer().read(cx).snapshot(cx);
1629    let creases = message_creases
1630        .iter()
1631        .map(|crease| {
1632            let start = buffer_snapshot.anchor_after(crease.range.start);
1633            let end = buffer_snapshot.anchor_before(crease.range.end);
1634            crease_for_mention(
1635                crease.label.clone(),
1636                crease.icon_path.clone(),
1637                None,
1638                start..end,
1639                cx.weak_entity(),
1640            )
1641        })
1642        .collect::<Vec<_>>();
1643    let ids = editor.insert_creases(creases.clone(), cx);
1644    editor.fold_creases(creases, false, window, cx);
1645    ids
1646}