message_editor.rs

  1use std::sync::Arc;
  2
  3use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
  4use fs::Fs;
  5use gpui::{
  6    AppContext, DismissEvent, FocusableView, Model, Subscription, TextStyle, View, WeakModel,
  7    WeakView,
  8};
  9use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
 10use language_model_selector::{LanguageModelSelector, LanguageModelSelectorPopoverMenu};
 11use rope::Point;
 12use settings::{update_settings_file, Settings};
 13use theme::ThemeSettings;
 14use ui::{
 15    prelude::*, ButtonLike, CheckboxWithLabel, ElevationIndex, KeyBinding, PopoverMenu,
 16    PopoverMenuHandle, Tooltip,
 17};
 18use workspace::Workspace;
 19
 20use crate::assistant_settings::AssistantSettings;
 21use crate::context_picker::{ConfirmBehavior, ContextPicker};
 22use crate::context_store::ContextStore;
 23use crate::context_strip::ContextStrip;
 24use crate::thread::{RequestKind, Thread};
 25use crate::thread_store::ThreadStore;
 26use crate::{Chat, ToggleContextPicker, ToggleModelSelector};
 27
 28pub struct MessageEditor {
 29    thread: Model<Thread>,
 30    editor: View<Editor>,
 31    context_store: Model<ContextStore>,
 32    context_strip: View<ContextStrip>,
 33    context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 34    inline_context_picker: View<ContextPicker>,
 35    inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
 36    language_model_selector: View<LanguageModelSelector>,
 37    language_model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
 38    use_tools: bool,
 39    _subscriptions: Vec<Subscription>,
 40}
 41
 42impl MessageEditor {
 43    pub fn new(
 44        fs: Arc<dyn Fs>,
 45        workspace: WeakView<Workspace>,
 46        thread_store: WeakModel<ThreadStore>,
 47        thread: Model<Thread>,
 48        cx: &mut ViewContext<Self>,
 49    ) -> Self {
 50        let context_store = cx.new_model(|_cx| ContextStore::new());
 51        let context_picker_menu_handle = PopoverMenuHandle::default();
 52        let inline_context_picker_menu_handle = PopoverMenuHandle::default();
 53
 54        let editor = cx.new_view(|cx| {
 55            let mut editor = Editor::auto_height(10, cx);
 56            editor.set_placeholder_text("Ask anything, @ to add context", cx);
 57            editor.set_show_indent_guides(false, cx);
 58
 59            editor
 60        });
 61        let inline_context_picker = cx.new_view(|cx| {
 62            ContextPicker::new(
 63                workspace.clone(),
 64                Some(thread_store.clone()),
 65                context_store.downgrade(),
 66                ConfirmBehavior::Close,
 67                cx,
 68            )
 69        });
 70        let subscriptions = vec![
 71            cx.subscribe(&editor, Self::handle_editor_event),
 72            cx.subscribe(
 73                &inline_context_picker,
 74                Self::handle_inline_context_picker_event,
 75            ),
 76        ];
 77
 78        Self {
 79            thread,
 80            editor: editor.clone(),
 81            context_store: context_store.clone(),
 82            context_strip: cx.new_view(|cx| {
 83                ContextStrip::new(
 84                    context_store,
 85                    workspace.clone(),
 86                    Some(thread_store.clone()),
 87                    editor.focus_handle(cx),
 88                    context_picker_menu_handle.clone(),
 89                    cx,
 90                )
 91            }),
 92            context_picker_menu_handle,
 93            inline_context_picker,
 94            inline_context_picker_menu_handle,
 95            language_model_selector: cx.new_view(|cx| {
 96                let fs = fs.clone();
 97                LanguageModelSelector::new(
 98                    move |model, cx| {
 99                        update_settings_file::<AssistantSettings>(
100                            fs.clone(),
101                            cx,
102                            move |settings, _cx| settings.set_model(model.clone()),
103                        );
104                    },
105                    cx,
106                )
107            }),
108            language_model_selector_menu_handle: PopoverMenuHandle::default(),
109            use_tools: false,
110            _subscriptions: subscriptions,
111        }
112    }
113
114    fn toggle_model_selector(&mut self, _: &ToggleModelSelector, cx: &mut ViewContext<Self>) {
115        self.language_model_selector_menu_handle.toggle(cx);
116    }
117
118    fn toggle_context_picker(&mut self, _: &ToggleContextPicker, cx: &mut ViewContext<Self>) {
119        self.context_picker_menu_handle.toggle(cx);
120    }
121
122    fn chat(&mut self, _: &Chat, cx: &mut ViewContext<Self>) {
123        self.send_to_model(RequestKind::Chat, cx);
124    }
125
126    fn send_to_model(
127        &mut self,
128        request_kind: RequestKind,
129        cx: &mut ViewContext<Self>,
130    ) -> Option<()> {
131        let provider = LanguageModelRegistry::read_global(cx).active_provider();
132        if provider
133            .as_ref()
134            .map_or(false, |provider| provider.must_accept_terms(cx))
135        {
136            cx.notify();
137            return None;
138        }
139
140        let model_registry = LanguageModelRegistry::read_global(cx);
141        let model = model_registry.active_model()?;
142
143        let user_message = self.editor.update(cx, |editor, cx| {
144            let text = editor.text(cx);
145            editor.clear(cx);
146            text
147        });
148        let context = self.context_store.update(cx, |this, _cx| this.drain());
149
150        self.thread.update(cx, |thread, cx| {
151            thread.insert_user_message(user_message, context, cx);
152            let mut request = thread.to_completion_request(request_kind, cx);
153
154            if self.use_tools {
155                request.tools = thread
156                    .tools()
157                    .tools(cx)
158                    .into_iter()
159                    .map(|tool| LanguageModelRequestTool {
160                        name: tool.name(),
161                        description: tool.description(),
162                        input_schema: tool.input_schema(),
163                    })
164                    .collect();
165            }
166
167            thread.stream_completion(request, model, cx)
168        });
169
170        None
171    }
172
173    fn handle_editor_event(
174        &mut self,
175        editor: View<Editor>,
176        event: &EditorEvent,
177        cx: &mut ViewContext<Self>,
178    ) {
179        match event {
180            EditorEvent::SelectionsChanged { .. } => {
181                editor.update(cx, |editor, cx| {
182                    let snapshot = editor.buffer().read(cx).snapshot(cx);
183                    let newest_cursor = editor.selections.newest::<Point>(cx).head();
184                    if newest_cursor.column > 0 {
185                        let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
186                        let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
187                        if char_behind_cursor == Some('@') {
188                            self.inline_context_picker_menu_handle.show(cx);
189                        }
190                    }
191                });
192            }
193            _ => {}
194        }
195    }
196
197    fn handle_inline_context_picker_event(
198        &mut self,
199        _inline_context_picker: View<ContextPicker>,
200        _event: &DismissEvent,
201        cx: &mut ViewContext<Self>,
202    ) {
203        let editor_focus_handle = self.editor.focus_handle(cx);
204        cx.focus(&editor_focus_handle);
205    }
206
207    fn render_language_model_selector(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
208        let active_model = LanguageModelRegistry::read_global(cx).active_model();
209        let focus_handle = self.language_model_selector.focus_handle(cx).clone();
210
211        LanguageModelSelectorPopoverMenu::new(
212            self.language_model_selector.clone(),
213            ButtonLike::new("active-model")
214                .style(ButtonStyle::Subtle)
215                .child(
216                    h_flex()
217                        .w_full()
218                        .gap_0p5()
219                        .child(
220                            div()
221                                .overflow_x_hidden()
222                                .flex_grow()
223                                .whitespace_nowrap()
224                                .child(match active_model {
225                                    Some(model) => h_flex()
226                                        .child(
227                                            Label::new(model.name().0)
228                                                .size(LabelSize::Small)
229                                                .color(Color::Muted),
230                                        )
231                                        .into_any_element(),
232                                    _ => Label::new("No model selected")
233                                        .size(LabelSize::Small)
234                                        .color(Color::Muted)
235                                        .into_any_element(),
236                                }),
237                        )
238                        .child(
239                            Icon::new(IconName::ChevronDown)
240                                .color(Color::Muted)
241                                .size(IconSize::XSmall),
242                        ),
243                )
244                .tooltip(move |cx| {
245                    Tooltip::for_action_in("Change Model", &ToggleModelSelector, &focus_handle, cx)
246                }),
247        )
248        .with_handle(self.language_model_selector_menu_handle.clone())
249    }
250}
251
252impl FocusableView for MessageEditor {
253    fn focus_handle(&self, cx: &AppContext) -> gpui::FocusHandle {
254        self.editor.focus_handle(cx)
255    }
256}
257
258impl Render for MessageEditor {
259    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
260        let font_size = TextSize::Default.rems(cx);
261        let line_height = font_size.to_pixels(cx.rem_size()) * 1.5;
262        let focus_handle = self.editor.focus_handle(cx);
263        let inline_context_picker = self.inline_context_picker.clone();
264        let bg_color = cx.theme().colors().editor_background;
265
266        v_flex()
267            .key_context("MessageEditor")
268            .on_action(cx.listener(Self::chat))
269            .on_action(cx.listener(Self::toggle_model_selector))
270            .on_action(cx.listener(Self::toggle_context_picker))
271            .size_full()
272            .gap_2()
273            .p_2()
274            .bg(bg_color)
275            .child(self.context_strip.clone())
276            .child({
277                let settings = ThemeSettings::get_global(cx);
278                let text_style = TextStyle {
279                    color: cx.theme().colors().editor_foreground,
280                    font_family: settings.ui_font.family.clone(),
281                    font_features: settings.ui_font.features.clone(),
282                    font_size: font_size.into(),
283                    font_weight: settings.ui_font.weight,
284                    line_height: line_height.into(),
285                    ..Default::default()
286                };
287
288                EditorElement::new(
289                    &self.editor,
290                    EditorStyle {
291                        background: bg_color,
292                        local_player: cx.theme().players().local(),
293                        text: text_style,
294                        ..Default::default()
295                    },
296                )
297            })
298            .child(
299                PopoverMenu::new("inline-context-picker")
300                    .menu(move |_cx| Some(inline_context_picker.clone()))
301                    .attach(gpui::Corner::TopLeft)
302                    .anchor(gpui::Corner::BottomLeft)
303                    .offset(gpui::Point {
304                        x: px(0.0),
305                        y: px(-16.0),
306                    })
307                    .with_handle(self.inline_context_picker_menu_handle.clone()),
308            )
309            .child(
310                h_flex()
311                    .justify_between()
312                    .child(CheckboxWithLabel::new(
313                        "use-tools",
314                        Label::new("Tools"),
315                        self.use_tools.into(),
316                        cx.listener(|this, selection, _cx| {
317                            this.use_tools = match selection {
318                                ToggleState::Selected => true,
319                                ToggleState::Unselected | ToggleState::Indeterminate => false,
320                            };
321                        }),
322                    ))
323                    .child(
324                        h_flex()
325                            .gap_1()
326                            .child(self.render_language_model_selector(cx))
327                            .child(
328                                ButtonLike::new("chat")
329                                    .style(ButtonStyle::Filled)
330                                    .layer(ElevationIndex::ModalSurface)
331                                    .child(Label::new("Submit"))
332                                    .children(
333                                        KeyBinding::for_action_in(&Chat, &focus_handle, cx)
334                                            .map(|binding| binding.into_any_element()),
335                                    )
336                                    .on_click(move |_event, cx| {
337                                        focus_handle.dispatch_action(&Chat, cx);
338                                    }),
339                            ),
340                    ),
341            )
342    }
343}