inline_prompt_editor.rs

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