Skip to repository content1827 lines · 69.7 KB · rust
tenant.openagents/omega
No repository description is available.
OpenAgents Git authority 2026-07-28T04:08:28.272Z Public web read
NIP-34 coordinate
30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omegaMaintainersHidden in public view
References2 branches · 1 tag
Read-only clone
git clone https://openagents.com/git/tenant.openagents/omega.gitBrowse files
inline_prompt_editor.rs
1use agent::ThreadStore;
2use agent_settings::AgentSettings;
3use collections::{HashMap, VecDeque};
4use editor::actions::Paste;
5use editor::code_context_menus::CodeContextMenu;
6use editor::display_map::{CreaseId, EditorMargins};
7use editor::{AnchorRangeExt as _, MultiBufferOffset, ToOffset as _};
8use editor::{
9 ContextMenuOptions, Editor, EditorElement, EditorEvent, EditorMode, EditorStyle, MultiBuffer,
10};
11use fs::Fs;
12use gpui::{
13 AnyElement, App, ClipboardItem, Context, Entity, EventEmitter, FocusHandle, Focusable,
14 Subscription, TextStyle, TextStyleRefinement, WeakEntity, Window, actions,
15};
16use language_model::{LanguageModel, LanguageModelRegistry};
17use markdown::{HeadingLevelStyles, Markdown, MarkdownElement, MarkdownStyle};
18use parking_lot::Mutex;
19use project::Project;
20use 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