Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T02:59:09.163Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

inline_prompt_editor.rs

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