inline_prompt_editor.rs

   1use agent::{
   2    context_store::ContextStore,
   3    thread_store::{TextThreadStore, ThreadStore},
   4};
   5use collections::VecDeque;
   6use editor::actions::Paste;
   7use editor::display_map::EditorMargins;
   8use editor::{
   9    ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
  10    actions::{MoveDown, MoveUp},
  11};
  12use fs::Fs;
  13use gpui::{
  14    AnyElement, App, ClipboardEntry, Context, CursorStyle, Entity, EventEmitter, FocusHandle,
  15    Focusable, Subscription, TextStyle, WeakEntity, Window,
  16};
  17use language_model::{LanguageModel, LanguageModelRegistry};
  18use parking_lot::Mutex;
  19use settings::Settings;
  20use std::cmp;
  21use std::rc::Rc;
  22use std::sync::Arc;
  23use theme::ThemeSettings;
  24use ui::utils::WithRemSize;
  25use ui::{IconButtonShape, KeyBinding, PopoverMenuHandle, Tooltip, prelude::*};
  26use workspace::Workspace;
  27use zed_actions::agent::ToggleModelSelector;
  28
  29use crate::agent_model_selector::AgentModelSelector;
  30use crate::buffer_codegen::BufferCodegen;
  31use crate::context_picker::{ContextPicker, ContextPickerCompletionProvider};
  32use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
  33use crate::message_editor::{ContextCreasesAddon, extract_message_creases, insert_message_creases};
  34use crate::terminal_codegen::TerminalCodegen;
  35use crate::{CycleNextInlineAssist, CyclePreviousInlineAssist, ModelUsageContext};
  36use crate::{RemoveAllContext, ToggleContextPicker};
  37
  38pub struct PromptEditor<T> {
  39    pub editor: Entity<Editor>,
  40    mode: PromptEditorMode,
  41    context_store: Entity<ContextStore>,
  42    context_strip: Entity<ContextStrip>,
  43    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
  44    model_selector: Entity<AgentModelSelector>,
  45    edited_since_done: bool,
  46    prompt_history: VecDeque<String>,
  47    prompt_history_ix: Option<usize>,
  48    pending_prompt: String,
  49    _codegen_subscription: Subscription,
  50    editor_subscriptions: Vec<Subscription>,
  51    _context_strip_subscription: Subscription,
  52    show_rate_limit_notice: bool,
  53    _phantom: std::marker::PhantomData<T>,
  54}
  55
  56impl<T: 'static> EventEmitter<PromptEditorEvent> for PromptEditor<T> {}
  57
  58impl<T: 'static> Render for PromptEditor<T> {
  59    fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
  60        let ui_font_size = ThemeSettings::get_global(cx).ui_font_size(cx);
  61        let mut buttons = Vec::new();
  62
  63        const RIGHT_PADDING: Pixels = px(9.);
  64
  65        let (left_gutter_width, right_padding) = match &self.mode {
  66            PromptEditorMode::Buffer {
  67                id: _,
  68                codegen,
  69                editor_margins,
  70            } => {
  71                let codegen = codegen.read(cx);
  72
  73                if codegen.alternative_count(cx) > 1 {
  74                    buttons.push(self.render_cycle_controls(codegen, cx));
  75                }
  76
  77                let editor_margins = editor_margins.lock();
  78                let gutter = editor_margins.gutter;
  79
  80                let left_gutter_width = gutter.full_width() + (gutter.margin / 2.0);
  81                let right_padding = editor_margins.right + RIGHT_PADDING;
  82
  83                (left_gutter_width, right_padding)
  84            }
  85            PromptEditorMode::Terminal { .. } => {
  86                // Give the equivalent of the same left-padding that we're using on the right
  87                (Pixels::from(40.0), Pixels::from(24.))
  88            }
  89        };
  90
  91        let bottom_padding = match &self.mode {
  92            PromptEditorMode::Buffer { .. } => rems_from_px(2.0),
  93            PromptEditorMode::Terminal { .. } => rems_from_px(8.0),
  94        };
  95
  96        buttons.extend(self.render_buttons(window, cx));
  97
  98        v_flex()
  99            .key_context("PromptEditor")
 100            .capture_action(cx.listener(Self::paste))
 101            .bg(cx.theme().colors().editor_background)
 102            .block_mouse_except_scroll()
 103            .gap_0p5()
 104            .border_y_1()
 105            .border_color(cx.theme().status().info_border)
 106            .size_full()
 107            .pt_0p5()
 108            .pb(bottom_padding)
 109            .pr(right_padding)
 110            .child(
 111                h_flex()
 112                    .items_start()
 113                    .cursor(CursorStyle::Arrow)
 114                    .on_action(cx.listener(Self::toggle_context_picker))
 115                    .on_action(cx.listener(|this, _: &ToggleModelSelector, window, cx| {
 116                        this.model_selector
 117                            .update(cx, |model_selector, cx| model_selector.toggle(window, cx));
 118                    }))
 119                    .on_action(cx.listener(Self::confirm))
 120                    .on_action(cx.listener(Self::cancel))
 121                    .on_action(cx.listener(Self::move_up))
 122                    .on_action(cx.listener(Self::move_down))
 123                    .on_action(cx.listener(Self::remove_all_context))
 124                    .capture_action(cx.listener(Self::cycle_prev))
 125                    .capture_action(cx.listener(Self::cycle_next))
 126                    .child(
 127                        WithRemSize::new(ui_font_size)
 128                            .flex()
 129                            .flex_row()
 130                            .flex_shrink_0()
 131                            .items_center()
 132                            .h_full()
 133                            .w(left_gutter_width)
 134                            .justify_center()
 135                            .gap_2()
 136                            .child(self.render_close_button(cx))
 137                            .map(|el| {
 138                                let CodegenStatus::Error(error) = self.codegen_status(cx) else {
 139                                    return el;
 140                                };
 141
 142                                let error_message = SharedString::from(error.to_string());
 143                                el.child(
 144                                    div()
 145                                        .id("error")
 146                                        .tooltip(Tooltip::text(error_message))
 147                                        .child(
 148                                            Icon::new(IconName::XCircle)
 149                                                .size(IconSize::Small)
 150                                                .color(Color::Error),
 151                                        ),
 152                                )
 153                            }),
 154                    )
 155                    .child(
 156                        h_flex()
 157                            .w_full()
 158                            .justify_between()
 159                            .child(div().flex_1().child(self.render_editor(window, cx)))
 160                            .child(
 161                                WithRemSize::new(ui_font_size)
 162                                    .flex()
 163                                    .flex_row()
 164                                    .items_center()
 165                                    .gap_1()
 166                                    .children(buttons),
 167                            ),
 168                    ),
 169            )
 170            .child(
 171                WithRemSize::new(ui_font_size)
 172                    .flex()
 173                    .flex_row()
 174                    .items_center()
 175                    .child(h_flex().flex_shrink_0().w(left_gutter_width))
 176                    .child(
 177                        h_flex()
 178                            .w_full()
 179                            .pl_1()
 180                            .items_start()
 181                            .justify_between()
 182                            .child(self.context_strip.clone())
 183                            .child(self.model_selector.clone()),
 184                    ),
 185            )
 186    }
 187}
 188
 189impl<T: 'static> Focusable for PromptEditor<T> {
 190    fn focus_handle(&self, cx: &App) -> FocusHandle {
 191        self.editor.focus_handle(cx)
 192    }
 193}
 194
 195impl<T: 'static> PromptEditor<T> {
 196    const MAX_LINES: u8 = 8;
 197
 198    fn codegen_status<'a>(&'a self, cx: &'a App) -> &'a CodegenStatus {
 199        match &self.mode {
 200            PromptEditorMode::Buffer { codegen, .. } => codegen.read(cx).status(cx),
 201            PromptEditorMode::Terminal { codegen, .. } => &codegen.read(cx).status,
 202        }
 203    }
 204
 205    fn subscribe_to_editor(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 206        self.editor_subscriptions.clear();
 207        self.editor_subscriptions.push(cx.subscribe_in(
 208            &self.editor,
 209            window,
 210            Self::handle_prompt_editor_events,
 211        ));
 212    }
 213
 214    pub fn set_show_cursor_when_unfocused(
 215        &mut self,
 216        show_cursor_when_unfocused: bool,
 217        cx: &mut Context<Self>,
 218    ) {
 219        self.editor.update(cx, |editor, cx| {
 220            editor.set_show_cursor_when_unfocused(show_cursor_when_unfocused, cx)
 221        });
 222    }
 223
 224    pub fn unlink(&mut self, window: &mut Window, cx: &mut Context<Self>) {
 225        let prompt = self.prompt(cx);
 226        let existing_creases = self.editor.update(cx, extract_message_creases);
 227
 228        let focus = self.editor.focus_handle(cx).contains_focused(window, cx);
 229        self.editor = cx.new(|cx| {
 230            let mut editor = Editor::auto_height(1, Self::MAX_LINES as usize, window, cx);
 231            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 232            editor.set_placeholder_text("Add a prompt…", window, cx);
 233            editor.set_text(prompt, window, cx);
 234            insert_message_creases(
 235                &mut editor,
 236                &existing_creases,
 237                &self.context_store,
 238                window,
 239                cx,
 240            );
 241
 242            if focus {
 243                window.focus(&editor.focus_handle(cx));
 244            }
 245            editor
 246        });
 247        self.subscribe_to_editor(window, cx);
 248    }
 249
 250    pub fn placeholder_text(mode: &PromptEditorMode, window: &mut Window, cx: &mut App) -> String {
 251        let action = match mode {
 252            PromptEditorMode::Buffer { codegen, .. } => {
 253                if codegen.read(cx).is_insertion {
 254                    "Generate"
 255                } else {
 256                    "Transform"
 257                }
 258            }
 259            PromptEditorMode::Terminal { .. } => "Generate",
 260        };
 261
 262        let agent_panel_keybinding =
 263            ui::text_for_action(&zed_actions::assistant::ToggleFocus, window, cx)
 264                .map(|keybinding| format!("{keybinding} to chat ― "))
 265                .unwrap_or_default();
 266
 267        format!("{action}… ({agent_panel_keybinding}↓↑ for history)")
 268    }
 269
 270    pub fn prompt(&self, cx: &App) -> String {
 271        self.editor.read(cx).text(cx)
 272    }
 273
 274    fn paste(&mut self, _: &Paste, _window: &mut Window, cx: &mut Context<Self>) {
 275        let images = cx
 276            .read_from_clipboard()
 277            .map(|item| {
 278                item.into_entries()
 279                    .filter_map(|entry| {
 280                        if let ClipboardEntry::Image(image) = entry {
 281                            Some(image)
 282                        } else {
 283                            None
 284                        }
 285                    })
 286                    .collect::<Vec<_>>()
 287            })
 288            .unwrap_or_default();
 289
 290        if images.is_empty() {
 291            return;
 292        }
 293        cx.stop_propagation();
 294
 295        self.context_store.update(cx, |store, cx| {
 296            for image in images {
 297                store.add_image_instance(Arc::new(image), cx);
 298            }
 299        });
 300    }
 301
 302    fn handle_prompt_editor_events(
 303        &mut self,
 304        _: &Entity<Editor>,
 305        event: &EditorEvent,
 306        window: &mut Window,
 307        cx: &mut Context<Self>,
 308    ) {
 309        match event {
 310            EditorEvent::Edited { .. } => {
 311                if let Some(workspace) = window.root::<Workspace>().flatten() {
 312                    workspace.update(cx, |workspace, cx| {
 313                        let is_via_ssh = workspace.project().read(cx).is_via_remote_server();
 314
 315                        workspace
 316                            .client()
 317                            .telemetry()
 318                            .log_edit_event("inline assist", is_via_ssh);
 319                    });
 320                }
 321                let prompt = self.editor.read(cx).text(cx);
 322                if self
 323                    .prompt_history_ix
 324                    .is_none_or(|ix| self.prompt_history[ix] != prompt)
 325                {
 326                    self.prompt_history_ix.take();
 327                    self.pending_prompt = prompt;
 328                }
 329
 330                self.edited_since_done = true;
 331                cx.notify();
 332            }
 333            EditorEvent::Blurred => {
 334                if self.show_rate_limit_notice {
 335                    self.show_rate_limit_notice = false;
 336                    cx.notify();
 337                }
 338            }
 339            _ => {}
 340        }
 341    }
 342
 343    fn toggle_context_picker(
 344        &mut self,
 345        _: &ToggleContextPicker,
 346        window: &mut Window,
 347        cx: &mut Context<Self>,
 348    ) {
 349        self.context_picker_menu_handle.toggle(window, cx);
 350    }
 351
 352    pub fn remove_all_context(
 353        &mut self,
 354        _: &RemoveAllContext,
 355        _window: &mut Window,
 356        cx: &mut Context<Self>,
 357    ) {
 358        self.context_store.update(cx, |store, cx| store.clear(cx));
 359        cx.notify();
 360    }
 361
 362    fn cancel(
 363        &mut self,
 364        _: &editor::actions::Cancel,
 365        _window: &mut Window,
 366        cx: &mut Context<Self>,
 367    ) {
 368        match self.codegen_status(cx) {
 369            CodegenStatus::Idle | CodegenStatus::Done | CodegenStatus::Error(_) => {
 370                cx.emit(PromptEditorEvent::CancelRequested);
 371            }
 372            CodegenStatus::Pending => {
 373                cx.emit(PromptEditorEvent::StopRequested);
 374            }
 375        }
 376    }
 377
 378    fn confirm(&mut self, _: &menu::Confirm, _window: &mut Window, cx: &mut Context<Self>) {
 379        match self.codegen_status(cx) {
 380            CodegenStatus::Idle => {
 381                cx.emit(PromptEditorEvent::StartRequested);
 382            }
 383            CodegenStatus::Pending => {}
 384            CodegenStatus::Done => {
 385                if self.edited_since_done {
 386                    cx.emit(PromptEditorEvent::StartRequested);
 387                } else {
 388                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 389                }
 390            }
 391            CodegenStatus::Error(_) => {
 392                cx.emit(PromptEditorEvent::StartRequested);
 393            }
 394        }
 395    }
 396
 397    fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
 398        if let Some(ix) = self.prompt_history_ix {
 399            if ix > 0 {
 400                self.prompt_history_ix = Some(ix - 1);
 401                let prompt = self.prompt_history[ix - 1].as_str();
 402                self.editor.update(cx, |editor, cx| {
 403                    editor.set_text(prompt, window, cx);
 404                    editor.move_to_beginning(&Default::default(), window, cx);
 405                });
 406            }
 407        } else if !self.prompt_history.is_empty() {
 408            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
 409            let prompt = self.prompt_history[self.prompt_history.len() - 1].as_str();
 410            self.editor.update(cx, |editor, cx| {
 411                editor.set_text(prompt, window, cx);
 412                editor.move_to_beginning(&Default::default(), window, cx);
 413            });
 414        }
 415    }
 416
 417    fn move_down(&mut self, _: &MoveDown, window: &mut Window, cx: &mut Context<Self>) {
 418        if let Some(ix) = self.prompt_history_ix {
 419            if ix < self.prompt_history.len() - 1 {
 420                self.prompt_history_ix = Some(ix + 1);
 421                let prompt = self.prompt_history[ix + 1].as_str();
 422                self.editor.update(cx, |editor, cx| {
 423                    editor.set_text(prompt, window, cx);
 424                    editor.move_to_end(&Default::default(), window, cx)
 425                });
 426            } else {
 427                self.prompt_history_ix = None;
 428                let prompt = self.pending_prompt.as_str();
 429                self.editor.update(cx, |editor, cx| {
 430                    editor.set_text(prompt, window, cx);
 431                    editor.move_to_end(&Default::default(), window, cx)
 432                });
 433            }
 434        } else if self.context_strip.read(cx).has_context_items(cx) {
 435            self.context_strip.focus_handle(cx).focus(window);
 436        }
 437    }
 438
 439    fn render_buttons(&self, _window: &mut Window, cx: &mut Context<Self>) -> Vec<AnyElement> {
 440        let mode = match &self.mode {
 441            PromptEditorMode::Buffer { codegen, .. } => {
 442                let codegen = codegen.read(cx);
 443                if codegen.is_insertion {
 444                    GenerationMode::Generate
 445                } else {
 446                    GenerationMode::Transform
 447                }
 448            }
 449            PromptEditorMode::Terminal { .. } => GenerationMode::Generate,
 450        };
 451
 452        let codegen_status = self.codegen_status(cx);
 453
 454        match codegen_status {
 455            CodegenStatus::Idle => {
 456                vec![
 457                    Button::new("start", mode.start_label())
 458                        .label_size(LabelSize::Small)
 459                        .icon(IconName::Return)
 460                        .icon_size(IconSize::XSmall)
 461                        .icon_color(Color::Muted)
 462                        .on_click(
 463                            cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StartRequested)),
 464                        )
 465                        .into_any_element(),
 466                ]
 467            }
 468            CodegenStatus::Pending => vec![
 469                IconButton::new("stop", IconName::Stop)
 470                    .icon_color(Color::Error)
 471                    .shape(IconButtonShape::Square)
 472                    .tooltip(move |window, cx| {
 473                        Tooltip::with_meta(
 474                            mode.tooltip_interrupt(),
 475                            Some(&menu::Cancel),
 476                            "Changes won't be discarded",
 477                            window,
 478                            cx,
 479                        )
 480                    })
 481                    .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::StopRequested)))
 482                    .into_any_element(),
 483            ],
 484            CodegenStatus::Done | CodegenStatus::Error(_) => {
 485                let has_error = matches!(codegen_status, CodegenStatus::Error(_));
 486                if has_error || self.edited_since_done {
 487                    vec![
 488                        IconButton::new("restart", IconName::RotateCw)
 489                            .icon_color(Color::Info)
 490                            .shape(IconButtonShape::Square)
 491                            .tooltip(move |window, cx| {
 492                                Tooltip::with_meta(
 493                                    mode.tooltip_restart(),
 494                                    Some(&menu::Confirm),
 495                                    "Changes will be discarded",
 496                                    window,
 497                                    cx,
 498                                )
 499                            })
 500                            .on_click(cx.listener(|_, _, _, cx| {
 501                                cx.emit(PromptEditorEvent::StartRequested);
 502                            }))
 503                            .into_any_element(),
 504                    ]
 505                } else {
 506                    let accept = IconButton::new("accept", IconName::Check)
 507                        .icon_color(Color::Info)
 508                        .shape(IconButtonShape::Square)
 509                        .tooltip(move |window, cx| {
 510                            Tooltip::for_action(mode.tooltip_accept(), &menu::Confirm, window, cx)
 511                        })
 512                        .on_click(cx.listener(|_, _, _, cx| {
 513                            cx.emit(PromptEditorEvent::ConfirmRequested { execute: false });
 514                        }))
 515                        .into_any_element();
 516
 517                    match &self.mode {
 518                        PromptEditorMode::Terminal { .. } => vec![
 519                            accept,
 520                            IconButton::new("confirm", IconName::PlayFilled)
 521                                .icon_color(Color::Info)
 522                                .shape(IconButtonShape::Square)
 523                                .tooltip(|window, cx| {
 524                                    Tooltip::for_action(
 525                                        "Execute Generated Command",
 526                                        &menu::SecondaryConfirm,
 527                                        window,
 528                                        cx,
 529                                    )
 530                                })
 531                                .on_click(cx.listener(|_, _, _, cx| {
 532                                    cx.emit(PromptEditorEvent::ConfirmRequested { execute: true });
 533                                }))
 534                                .into_any_element(),
 535                        ],
 536                        PromptEditorMode::Buffer { .. } => vec![accept],
 537                    }
 538                }
 539            }
 540        }
 541    }
 542
 543    fn cycle_prev(
 544        &mut self,
 545        _: &CyclePreviousInlineAssist,
 546        _: &mut Window,
 547        cx: &mut Context<Self>,
 548    ) {
 549        match &self.mode {
 550            PromptEditorMode::Buffer { codegen, .. } => {
 551                codegen.update(cx, |codegen, cx| codegen.cycle_prev(cx));
 552            }
 553            PromptEditorMode::Terminal { .. } => {
 554                // no cycle buttons in terminal mode
 555            }
 556        }
 557    }
 558
 559    fn cycle_next(&mut self, _: &CycleNextInlineAssist, _: &mut Window, cx: &mut Context<Self>) {
 560        match &self.mode {
 561            PromptEditorMode::Buffer { codegen, .. } => {
 562                codegen.update(cx, |codegen, cx| codegen.cycle_next(cx));
 563            }
 564            PromptEditorMode::Terminal { .. } => {
 565                // no cycle buttons in terminal mode
 566            }
 567        }
 568    }
 569
 570    fn render_close_button(&self, cx: &mut Context<Self>) -> AnyElement {
 571        IconButton::new("cancel", IconName::Close)
 572            .icon_color(Color::Muted)
 573            .shape(IconButtonShape::Square)
 574            .tooltip(Tooltip::text("Close Assistant"))
 575            .on_click(cx.listener(|_, _, _, cx| cx.emit(PromptEditorEvent::CancelRequested)))
 576            .into_any_element()
 577    }
 578
 579    fn render_cycle_controls(&self, codegen: &BufferCodegen, cx: &Context<Self>) -> AnyElement {
 580        let disabled = matches!(codegen.status(cx), CodegenStatus::Idle);
 581
 582        let model_registry = LanguageModelRegistry::read_global(cx);
 583        let default_model = model_registry.default_model().map(|default| default.model);
 584        let alternative_models = model_registry.inline_alternative_models();
 585
 586        let get_model_name = |index: usize| -> String {
 587            let name = |model: &Arc<dyn LanguageModel>| model.name().0.to_string();
 588
 589            match index {
 590                0 => default_model.as_ref().map_or_else(String::new, name),
 591                index if index <= alternative_models.len() => alternative_models
 592                    .get(index - 1)
 593                    .map_or_else(String::new, name),
 594                _ => String::new(),
 595            }
 596        };
 597
 598        let total_models = alternative_models.len() + 1;
 599
 600        if total_models <= 1 {
 601            return div().into_any_element();
 602        }
 603
 604        let current_index = codegen.active_alternative;
 605        let prev_index = (current_index + total_models - 1) % total_models;
 606        let next_index = (current_index + 1) % total_models;
 607
 608        let prev_model_name = get_model_name(prev_index);
 609        let next_model_name = get_model_name(next_index);
 610
 611        h_flex()
 612            .child(
 613                IconButton::new("previous", IconName::ChevronLeft)
 614                    .icon_color(Color::Muted)
 615                    .disabled(disabled || current_index == 0)
 616                    .shape(IconButtonShape::Square)
 617                    .tooltip({
 618                        let focus_handle = self.editor.focus_handle(cx);
 619                        move |window, cx| {
 620                            cx.new(|cx| {
 621                                let mut tooltip = Tooltip::new("Previous Alternative").key_binding(
 622                                    KeyBinding::for_action_in(
 623                                        &CyclePreviousInlineAssist,
 624                                        &focus_handle,
 625                                        window,
 626                                        cx,
 627                                    ),
 628                                );
 629                                if !disabled && current_index != 0 {
 630                                    tooltip = tooltip.meta(prev_model_name.clone());
 631                                }
 632                                tooltip
 633                            })
 634                            .into()
 635                        }
 636                    })
 637                    .on_click(cx.listener(|this, _, window, cx| {
 638                        this.cycle_prev(&CyclePreviousInlineAssist, window, cx);
 639                    })),
 640            )
 641            .child(
 642                Label::new(format!(
 643                    "{}/{}",
 644                    codegen.active_alternative + 1,
 645                    codegen.alternative_count(cx)
 646                ))
 647                .size(LabelSize::Small)
 648                .color(if disabled {
 649                    Color::Disabled
 650                } else {
 651                    Color::Muted
 652                }),
 653            )
 654            .child(
 655                IconButton::new("next", IconName::ChevronRight)
 656                    .icon_color(Color::Muted)
 657                    .disabled(disabled || current_index == total_models - 1)
 658                    .shape(IconButtonShape::Square)
 659                    .tooltip({
 660                        let focus_handle = self.editor.focus_handle(cx);
 661                        move |window, cx| {
 662                            cx.new(|cx| {
 663                                let mut tooltip = Tooltip::new("Next Alternative").key_binding(
 664                                    KeyBinding::for_action_in(
 665                                        &CycleNextInlineAssist,
 666                                        &focus_handle,
 667                                        window,
 668                                        cx,
 669                                    ),
 670                                );
 671                                if !disabled && current_index != total_models - 1 {
 672                                    tooltip = tooltip.meta(next_model_name.clone());
 673                                }
 674                                tooltip
 675                            })
 676                            .into()
 677                        }
 678                    })
 679                    .on_click(cx.listener(|this, _, window, cx| {
 680                        this.cycle_next(&CycleNextInlineAssist, window, cx)
 681                    })),
 682            )
 683            .into_any_element()
 684    }
 685
 686    fn render_editor(&mut self, _window: &mut Window, cx: &mut Context<Self>) -> AnyElement {
 687        let colors = cx.theme().colors();
 688
 689        div()
 690            .key_context("InlineAssistEditor")
 691            .size_full()
 692            .p_2()
 693            .pl_1()
 694            .bg(colors.editor_background)
 695            .child({
 696                let settings = ThemeSettings::get_global(cx);
 697                let font_size = settings.buffer_font_size(cx);
 698                let line_height = font_size * 1.2;
 699
 700                let text_style = TextStyle {
 701                    color: colors.editor_foreground,
 702                    font_family: settings.buffer_font.family.clone(),
 703                    font_features: settings.buffer_font.features.clone(),
 704                    font_size: font_size.into(),
 705                    line_height: line_height.into(),
 706                    ..Default::default()
 707                };
 708
 709                EditorElement::new(
 710                    &self.editor,
 711                    EditorStyle {
 712                        background: colors.editor_background,
 713                        local_player: cx.theme().players().local(),
 714                        text: text_style,
 715                        ..Default::default()
 716                    },
 717                )
 718            })
 719            .into_any_element()
 720    }
 721
 722    fn handle_context_strip_event(
 723        &mut self,
 724        _context_strip: &Entity<ContextStrip>,
 725        event: &ContextStripEvent,
 726        window: &mut Window,
 727        cx: &mut Context<Self>,
 728    ) {
 729        match event {
 730            ContextStripEvent::PickerDismissed
 731            | ContextStripEvent::BlurredEmpty
 732            | ContextStripEvent::BlurredUp => self.editor.focus_handle(cx).focus(window),
 733            ContextStripEvent::BlurredDown => {}
 734        }
 735    }
 736}
 737
 738pub enum PromptEditorMode {
 739    Buffer {
 740        id: InlineAssistId,
 741        codegen: Entity<BufferCodegen>,
 742        editor_margins: Arc<Mutex<EditorMargins>>,
 743    },
 744    Terminal {
 745        id: TerminalInlineAssistId,
 746        codegen: Entity<TerminalCodegen>,
 747        height_in_lines: u8,
 748    },
 749}
 750
 751pub enum PromptEditorEvent {
 752    StartRequested,
 753    StopRequested,
 754    ConfirmRequested { execute: bool },
 755    CancelRequested,
 756    Resized { height_in_lines: u8 },
 757}
 758
 759#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 760pub struct InlineAssistId(pub usize);
 761
 762impl InlineAssistId {
 763    pub fn post_inc(&mut self) -> InlineAssistId {
 764        let id = *self;
 765        self.0 += 1;
 766        id
 767    }
 768}
 769
 770impl PromptEditor<BufferCodegen> {
 771    pub fn new_buffer(
 772        id: InlineAssistId,
 773        editor_margins: Arc<Mutex<EditorMargins>>,
 774        prompt_history: VecDeque<String>,
 775        prompt_buffer: Entity<MultiBuffer>,
 776        codegen: Entity<BufferCodegen>,
 777        fs: Arc<dyn Fs>,
 778        context_store: Entity<ContextStore>,
 779        workspace: WeakEntity<Workspace>,
 780        thread_store: Option<WeakEntity<ThreadStore>>,
 781        text_thread_store: Option<WeakEntity<TextThreadStore>>,
 782        window: &mut Window,
 783        cx: &mut Context<PromptEditor<BufferCodegen>>,
 784    ) -> PromptEditor<BufferCodegen> {
 785        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 786        let codegen_buffer = codegen.read(cx).buffer(cx).read(cx).as_singleton();
 787        let mode = PromptEditorMode::Buffer {
 788            id,
 789            codegen,
 790            editor_margins,
 791        };
 792
 793        let prompt_editor = cx.new(|cx| {
 794            let mut editor = Editor::new(
 795                EditorMode::AutoHeight {
 796                    min_lines: 1,
 797                    max_lines: Some(Self::MAX_LINES as usize),
 798                },
 799                prompt_buffer,
 800                None,
 801                window,
 802                cx,
 803            );
 804            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 805            // Since the prompt editors for all inline assistants are linked,
 806            // always show the cursor (even when it isn't focused) because
 807            // typing in one will make what you typed appear in all of them.
 808            editor.set_show_cursor_when_unfocused(true, cx);
 809            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
 810            editor.register_addon(ContextCreasesAddon::new());
 811            editor.set_context_menu_options(ContextMenuOptions {
 812                min_entries_visible: 12,
 813                max_entries_visible: 12,
 814                placement: None,
 815            });
 816
 817            editor
 818        });
 819
 820        let prompt_editor_entity = prompt_editor.downgrade();
 821        prompt_editor.update(cx, |editor, _| {
 822            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
 823                workspace.clone(),
 824                context_store.downgrade(),
 825                thread_store.clone(),
 826                text_thread_store.clone(),
 827                prompt_editor_entity,
 828                codegen_buffer.as_ref().map(Entity::downgrade),
 829            ))));
 830        });
 831
 832        let context_picker_menu_handle = PopoverMenuHandle::default();
 833        let model_selector_menu_handle = PopoverMenuHandle::default();
 834
 835        let context_strip = cx.new(|cx| {
 836            ContextStrip::new(
 837                context_store.clone(),
 838                workspace.clone(),
 839                thread_store.clone(),
 840                text_thread_store.clone(),
 841                context_picker_menu_handle.clone(),
 842                SuggestContextKind::Thread,
 843                ModelUsageContext::InlineAssistant,
 844                window,
 845                cx,
 846            )
 847        });
 848
 849        let context_strip_subscription =
 850            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
 851
 852        let mut this: PromptEditor<BufferCodegen> = PromptEditor {
 853            editor: prompt_editor.clone(),
 854            context_store,
 855            context_strip,
 856            context_picker_menu_handle,
 857            model_selector: cx.new(|cx| {
 858                AgentModelSelector::new(
 859                    fs,
 860                    model_selector_menu_handle,
 861                    prompt_editor.focus_handle(cx),
 862                    ModelUsageContext::InlineAssistant,
 863                    window,
 864                    cx,
 865                )
 866            }),
 867            edited_since_done: false,
 868            prompt_history,
 869            prompt_history_ix: None,
 870            pending_prompt: String::new(),
 871            _codegen_subscription: codegen_subscription,
 872            editor_subscriptions: Vec::new(),
 873            _context_strip_subscription: context_strip_subscription,
 874            show_rate_limit_notice: false,
 875            mode,
 876            _phantom: Default::default(),
 877        };
 878
 879        this.subscribe_to_editor(window, cx);
 880        this
 881    }
 882
 883    fn handle_codegen_changed(
 884        &mut self,
 885        _: Entity<BufferCodegen>,
 886        cx: &mut Context<PromptEditor<BufferCodegen>>,
 887    ) {
 888        match self.codegen_status(cx) {
 889            CodegenStatus::Idle => {
 890                self.editor
 891                    .update(cx, |editor, _| editor.set_read_only(false));
 892            }
 893            CodegenStatus::Pending => {
 894                self.editor
 895                    .update(cx, |editor, _| editor.set_read_only(true));
 896            }
 897            CodegenStatus::Done => {
 898                self.edited_since_done = false;
 899                self.editor
 900                    .update(cx, |editor, _| editor.set_read_only(false));
 901            }
 902            CodegenStatus::Error(_error) => {
 903                self.edited_since_done = false;
 904                self.editor
 905                    .update(cx, |editor, _| editor.set_read_only(false));
 906            }
 907        }
 908    }
 909
 910    pub fn id(&self) -> InlineAssistId {
 911        match &self.mode {
 912            PromptEditorMode::Buffer { id, .. } => *id,
 913            PromptEditorMode::Terminal { .. } => unreachable!(),
 914        }
 915    }
 916
 917    pub fn codegen(&self) -> &Entity<BufferCodegen> {
 918        match &self.mode {
 919            PromptEditorMode::Buffer { codegen, .. } => codegen,
 920            PromptEditorMode::Terminal { .. } => unreachable!(),
 921        }
 922    }
 923
 924    pub fn editor_margins(&self) -> &Arc<Mutex<EditorMargins>> {
 925        match &self.mode {
 926            PromptEditorMode::Buffer { editor_margins, .. } => editor_margins,
 927            PromptEditorMode::Terminal { .. } => unreachable!(),
 928        }
 929    }
 930}
 931
 932#[derive(Copy, Clone, Default, Debug, PartialEq, Eq, Hash)]
 933pub struct TerminalInlineAssistId(pub usize);
 934
 935impl TerminalInlineAssistId {
 936    pub fn post_inc(&mut self) -> TerminalInlineAssistId {
 937        let id = *self;
 938        self.0 += 1;
 939        id
 940    }
 941}
 942
 943impl PromptEditor<TerminalCodegen> {
 944    pub fn new_terminal(
 945        id: TerminalInlineAssistId,
 946        prompt_history: VecDeque<String>,
 947        prompt_buffer: Entity<MultiBuffer>,
 948        codegen: Entity<TerminalCodegen>,
 949        fs: Arc<dyn Fs>,
 950        context_store: Entity<ContextStore>,
 951        workspace: WeakEntity<Workspace>,
 952        thread_store: Option<WeakEntity<ThreadStore>>,
 953        text_thread_store: Option<WeakEntity<TextThreadStore>>,
 954        window: &mut Window,
 955        cx: &mut Context<Self>,
 956    ) -> Self {
 957        let codegen_subscription = cx.observe(&codegen, Self::handle_codegen_changed);
 958        let mode = PromptEditorMode::Terminal {
 959            id,
 960            codegen,
 961            height_in_lines: 1,
 962        };
 963
 964        let prompt_editor = cx.new(|cx| {
 965            let mut editor = Editor::new(
 966                EditorMode::AutoHeight {
 967                    min_lines: 1,
 968                    max_lines: Some(Self::MAX_LINES as usize),
 969                },
 970                prompt_buffer,
 971                None,
 972                window,
 973                cx,
 974            );
 975            editor.set_soft_wrap_mode(language::language_settings::SoftWrap::EditorWidth, cx);
 976            editor.set_placeholder_text(&Self::placeholder_text(&mode, window, cx), window, cx);
 977            editor.set_context_menu_options(ContextMenuOptions {
 978                min_entries_visible: 12,
 979                max_entries_visible: 12,
 980                placement: None,
 981            });
 982            editor
 983        });
 984
 985        let prompt_editor_entity = prompt_editor.downgrade();
 986        prompt_editor.update(cx, |editor, _| {
 987            editor.set_completion_provider(Some(Rc::new(ContextPickerCompletionProvider::new(
 988                workspace.clone(),
 989                context_store.downgrade(),
 990                thread_store.clone(),
 991                text_thread_store.clone(),
 992                prompt_editor_entity,
 993                None,
 994            ))));
 995        });
 996
 997        let context_picker_menu_handle = PopoverMenuHandle::default();
 998        let model_selector_menu_handle = PopoverMenuHandle::default();
 999
1000        let context_strip = cx.new(|cx| {
1001            ContextStrip::new(
1002                context_store.clone(),
1003                workspace.clone(),
1004                thread_store.clone(),
1005                text_thread_store.clone(),
1006                context_picker_menu_handle.clone(),
1007                SuggestContextKind::Thread,
1008                ModelUsageContext::InlineAssistant,
1009                window,
1010                cx,
1011            )
1012        });
1013
1014        let context_strip_subscription =
1015            cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event);
1016
1017        let mut this = Self {
1018            editor: prompt_editor.clone(),
1019            context_store,
1020            context_strip,
1021            context_picker_menu_handle,
1022            model_selector: cx.new(|cx| {
1023                AgentModelSelector::new(
1024                    fs,
1025                    model_selector_menu_handle.clone(),
1026                    prompt_editor.focus_handle(cx),
1027                    ModelUsageContext::InlineAssistant,
1028                    window,
1029                    cx,
1030                )
1031            }),
1032            edited_since_done: false,
1033            prompt_history,
1034            prompt_history_ix: None,
1035            pending_prompt: String::new(),
1036            _codegen_subscription: codegen_subscription,
1037            editor_subscriptions: Vec::new(),
1038            _context_strip_subscription: context_strip_subscription,
1039            mode,
1040            show_rate_limit_notice: false,
1041            _phantom: Default::default(),
1042        };
1043        this.count_lines(cx);
1044        this.subscribe_to_editor(window, cx);
1045        this
1046    }
1047
1048    fn count_lines(&mut self, cx: &mut Context<Self>) {
1049        let height_in_lines = cmp::max(
1050            2, // Make the editor at least two lines tall, to account for padding and buttons.
1051            cmp::min(
1052                self.editor
1053                    .update(cx, |editor, cx| editor.max_point(cx).row().0 + 1),
1054                Self::MAX_LINES as u32,
1055            ),
1056        ) as u8;
1057
1058        match &mut self.mode {
1059            PromptEditorMode::Terminal {
1060                height_in_lines: current_height,
1061                ..
1062            } => {
1063                if height_in_lines != *current_height {
1064                    *current_height = height_in_lines;
1065                    cx.emit(PromptEditorEvent::Resized { height_in_lines });
1066                }
1067            }
1068            PromptEditorMode::Buffer { .. } => unreachable!(),
1069        }
1070    }
1071
1072    fn handle_codegen_changed(&mut self, _: Entity<TerminalCodegen>, cx: &mut Context<Self>) {
1073        match &self.codegen().read(cx).status {
1074            CodegenStatus::Idle => {
1075                self.editor
1076                    .update(cx, |editor, _| editor.set_read_only(false));
1077            }
1078            CodegenStatus::Pending => {
1079                self.editor
1080                    .update(cx, |editor, _| editor.set_read_only(true));
1081            }
1082            CodegenStatus::Done | CodegenStatus::Error(_) => {
1083                self.edited_since_done = false;
1084                self.editor
1085                    .update(cx, |editor, _| editor.set_read_only(false));
1086            }
1087        }
1088    }
1089
1090    pub fn codegen(&self) -> &Entity<TerminalCodegen> {
1091        match &self.mode {
1092            PromptEditorMode::Buffer { .. } => unreachable!(),
1093            PromptEditorMode::Terminal { codegen, .. } => codegen,
1094        }
1095    }
1096
1097    pub fn id(&self) -> TerminalInlineAssistId {
1098        match &self.mode {
1099            PromptEditorMode::Buffer { .. } => unreachable!(),
1100            PromptEditorMode::Terminal { id, .. } => *id,
1101        }
1102    }
1103}
1104
1105pub enum CodegenStatus {
1106    Idle,
1107    Pending,
1108    Done,
1109    Error(anyhow::Error),
1110}
1111
1112/// This is just CodegenStatus without the anyhow::Error, which causes a lifetime issue for rendering the Cancel button.
1113#[derive(Copy, Clone)]
1114pub enum CancelButtonState {
1115    Idle,
1116    Pending,
1117    Done,
1118    Error,
1119}
1120
1121impl Into<CancelButtonState> for &CodegenStatus {
1122    fn into(self) -> CancelButtonState {
1123        match self {
1124            CodegenStatus::Idle => CancelButtonState::Idle,
1125            CodegenStatus::Pending => CancelButtonState::Pending,
1126            CodegenStatus::Done => CancelButtonState::Done,
1127            CodegenStatus::Error(_) => CancelButtonState::Error,
1128        }
1129    }
1130}
1131
1132#[derive(Copy, Clone)]
1133pub enum GenerationMode {
1134    Generate,
1135    Transform,
1136}
1137
1138impl GenerationMode {
1139    fn start_label(self) -> &'static str {
1140        match self {
1141            GenerationMode::Generate => "Generate",
1142            GenerationMode::Transform => "Transform",
1143        }
1144    }
1145    fn tooltip_interrupt(self) -> &'static str {
1146        match self {
1147            GenerationMode::Generate => "Interrupt Generation",
1148            GenerationMode::Transform => "Interrupt Transform",
1149        }
1150    }
1151
1152    fn tooltip_restart(self) -> &'static str {
1153        match self {
1154            GenerationMode::Generate => "Restart Generation",
1155            GenerationMode::Transform => "Restart Transform",
1156        }
1157    }
1158
1159    fn tooltip_accept(self) -> &'static str {
1160        match self {
1161            GenerationMode::Generate => "Accept Generation",
1162            GenerationMode::Transform => "Accept Transform",
1163        }
1164    }
1165}