1use std::sync::Arc;
2
3use editor::actions::MoveUp;
4use editor::{Editor, EditorElement, EditorEvent, EditorStyle};
5use fs::Fs;
6use gpui::{
7 pulsating_between, Animation, AnimationExt, App, DismissEvent, Entity, Focusable, Subscription,
8 TextStyle, WeakEntity,
9};
10use language_model::{LanguageModelRegistry, LanguageModelRequestTool};
11use language_model_selector::LanguageModelSelector;
12use rope::Point;
13use settings::Settings;
14use std::time::Duration;
15use theme::ThemeSettings;
16use ui::{
17 prelude::*, ButtonLike, KeyBinding, PopoverMenu, PopoverMenuHandle, Switch, TintColor, Tooltip,
18};
19use workspace::Workspace;
20
21use crate::assistant_model_selector::AssistantModelSelector;
22use crate::context_picker::{ConfirmBehavior, ContextPicker};
23use crate::context_store::{refresh_context_store_text, ContextStore};
24use crate::context_strip::{ContextStrip, ContextStripEvent, SuggestContextKind};
25use crate::thread::{RequestKind, Thread};
26use crate::thread_store::ThreadStore;
27use crate::{Chat, ChatMode, RemoveAllContext, ToggleContextPicker, ToggleModelSelector};
28
29pub struct MessageEditor {
30 thread: Entity<Thread>,
31 editor: Entity<Editor>,
32 context_store: Entity<ContextStore>,
33 context_strip: Entity<ContextStrip>,
34 context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
35 inline_context_picker: Entity<ContextPicker>,
36 inline_context_picker_menu_handle: PopoverMenuHandle<ContextPicker>,
37 model_selector: Entity<AssistantModelSelector>,
38 model_selector_menu_handle: PopoverMenuHandle<LanguageModelSelector>,
39 use_tools: bool,
40 _subscriptions: Vec<Subscription>,
41}
42
43impl MessageEditor {
44 pub fn new(
45 fs: Arc<dyn Fs>,
46 workspace: WeakEntity<Workspace>,
47 thread_store: WeakEntity<ThreadStore>,
48 thread: Entity<Thread>,
49 window: &mut Window,
50 cx: &mut Context<Self>,
51 ) -> Self {
52 let context_store = cx.new(|_cx| ContextStore::new(workspace.clone()));
53 let context_picker_menu_handle = PopoverMenuHandle::default();
54 let inline_context_picker_menu_handle = PopoverMenuHandle::default();
55 let model_selector_menu_handle = PopoverMenuHandle::default();
56
57 let editor = cx.new(|cx| {
58 let mut editor = Editor::auto_height(10, window, cx);
59 editor.set_placeholder_text("Ask anything, @ to mention, ↑ to select", cx);
60 editor.set_show_indent_guides(false, cx);
61
62 editor
63 });
64
65 let inline_context_picker = cx.new(|cx| {
66 ContextPicker::new(
67 workspace.clone(),
68 Some(thread_store.clone()),
69 context_store.downgrade(),
70 editor.downgrade(),
71 ConfirmBehavior::Close,
72 window,
73 cx,
74 )
75 });
76
77 let context_strip = cx.new(|cx| {
78 ContextStrip::new(
79 context_store.clone(),
80 workspace.clone(),
81 editor.downgrade(),
82 Some(thread_store.clone()),
83 context_picker_menu_handle.clone(),
84 SuggestContextKind::File,
85 window,
86 cx,
87 )
88 });
89
90 let subscriptions = vec![
91 cx.subscribe_in(&editor, window, Self::handle_editor_event),
92 cx.subscribe_in(
93 &inline_context_picker,
94 window,
95 Self::handle_inline_context_picker_event,
96 ),
97 cx.subscribe_in(&context_strip, window, Self::handle_context_strip_event),
98 ];
99
100 Self {
101 thread,
102 editor: editor.clone(),
103 context_store,
104 context_strip,
105 context_picker_menu_handle,
106 inline_context_picker,
107 inline_context_picker_menu_handle,
108 model_selector: cx.new(|cx| {
109 AssistantModelSelector::new(
110 fs,
111 model_selector_menu_handle.clone(),
112 editor.focus_handle(cx),
113 window,
114 cx,
115 )
116 }),
117 model_selector_menu_handle,
118 use_tools: false,
119 _subscriptions: subscriptions,
120 }
121 }
122
123 fn toggle_model_selector(
124 &mut self,
125 _: &ToggleModelSelector,
126 window: &mut Window,
127 cx: &mut Context<Self>,
128 ) {
129 self.model_selector_menu_handle.toggle(window, cx)
130 }
131
132 fn toggle_chat_mode(&mut self, _: &ChatMode, _window: &mut Window, cx: &mut Context<Self>) {
133 self.use_tools = !self.use_tools;
134 cx.notify();
135 }
136
137 fn toggle_context_picker(
138 &mut self,
139 _: &ToggleContextPicker,
140 window: &mut Window,
141 cx: &mut Context<Self>,
142 ) {
143 self.context_picker_menu_handle.toggle(window, cx);
144 }
145
146 pub fn remove_all_context(
147 &mut self,
148 _: &RemoveAllContext,
149 _window: &mut Window,
150 cx: &mut Context<Self>,
151 ) {
152 self.context_store.update(cx, |store, _cx| store.clear());
153 cx.notify();
154 }
155
156 fn chat(&mut self, _: &Chat, window: &mut Window, cx: &mut Context<Self>) {
157 self.send_to_model(RequestKind::Chat, window, cx);
158 }
159
160 fn is_editor_empty(&self, cx: &App) -> bool {
161 self.editor.read(cx).text(cx).is_empty()
162 }
163
164 fn is_model_selected(&self, cx: &App) -> bool {
165 LanguageModelRegistry::read_global(cx)
166 .active_model()
167 .is_some()
168 }
169
170 fn send_to_model(
171 &mut self,
172 request_kind: RequestKind,
173 window: &mut Window,
174 cx: &mut Context<Self>,
175 ) {
176 let provider = LanguageModelRegistry::read_global(cx).active_provider();
177 if provider
178 .as_ref()
179 .map_or(false, |provider| provider.must_accept_terms(cx))
180 {
181 cx.notify();
182 return;
183 }
184
185 let model_registry = LanguageModelRegistry::read_global(cx);
186 let Some(model) = model_registry.active_model() else {
187 return;
188 };
189
190 let user_message = self.editor.update(cx, |editor, cx| {
191 let text = editor.text(cx);
192 editor.clear(window, cx);
193 text
194 });
195
196 let refresh_task = refresh_context_store_text(self.context_store.clone(), cx);
197
198 let thread = self.thread.clone();
199 let context_store = self.context_store.clone();
200 let use_tools = self.use_tools;
201 cx.spawn(move |_, mut cx| async move {
202 refresh_task.await;
203 thread
204 .update(&mut cx, |thread, cx| {
205 let context = context_store.read(cx).snapshot(cx).collect::<Vec<_>>();
206 thread.insert_user_message(user_message, context, cx);
207 let mut request = thread.to_completion_request(request_kind, cx);
208
209 if use_tools {
210 request.tools = thread
211 .tools()
212 .tools(cx)
213 .into_iter()
214 .map(|tool| LanguageModelRequestTool {
215 name: tool.name(),
216 description: tool.description(),
217 input_schema: tool.input_schema(),
218 })
219 .collect();
220 }
221
222 thread.stream_completion(request, model, cx)
223 })
224 .ok();
225 })
226 .detach();
227 }
228
229 fn handle_editor_event(
230 &mut self,
231 editor: &Entity<Editor>,
232 event: &EditorEvent,
233 window: &mut Window,
234 cx: &mut Context<Self>,
235 ) {
236 match event {
237 EditorEvent::SelectionsChanged { .. } => {
238 editor.update(cx, |editor, cx| {
239 let snapshot = editor.buffer().read(cx).snapshot(cx);
240 let newest_cursor = editor.selections.newest::<Point>(cx).head();
241 if newest_cursor.column > 0 {
242 let behind_cursor = Point::new(newest_cursor.row, newest_cursor.column - 1);
243 let char_behind_cursor = snapshot.chars_at(behind_cursor).next();
244 if char_behind_cursor == Some('@') {
245 self.inline_context_picker_menu_handle.show(window, cx);
246 }
247 }
248 });
249 }
250 _ => {}
251 }
252 }
253
254 fn handle_inline_context_picker_event(
255 &mut self,
256 _inline_context_picker: &Entity<ContextPicker>,
257 _event: &DismissEvent,
258 window: &mut Window,
259 cx: &mut Context<Self>,
260 ) {
261 let editor_focus_handle = self.editor.focus_handle(cx);
262 window.focus(&editor_focus_handle);
263 }
264
265 fn handle_context_strip_event(
266 &mut self,
267 _context_strip: &Entity<ContextStrip>,
268 event: &ContextStripEvent,
269 window: &mut Window,
270 cx: &mut Context<Self>,
271 ) {
272 match event {
273 ContextStripEvent::PickerDismissed
274 | ContextStripEvent::BlurredEmpty
275 | ContextStripEvent::BlurredDown => {
276 let editor_focus_handle = self.editor.focus_handle(cx);
277 window.focus(&editor_focus_handle);
278 }
279 ContextStripEvent::BlurredUp => {}
280 }
281 }
282
283 fn move_up(&mut self, _: &MoveUp, window: &mut Window, cx: &mut Context<Self>) {
284 if self.context_picker_menu_handle.is_deployed()
285 || self.inline_context_picker_menu_handle.is_deployed()
286 {
287 cx.propagate();
288 } else {
289 self.context_strip.focus_handle(cx).focus(window);
290 }
291 }
292}
293
294impl Focusable for MessageEditor {
295 fn focus_handle(&self, cx: &App) -> gpui::FocusHandle {
296 self.editor.focus_handle(cx)
297 }
298}
299
300impl Render for MessageEditor {
301 fn render(&mut self, window: &mut Window, cx: &mut Context<Self>) -> impl IntoElement {
302 let font_size = TextSize::Default.rems(cx);
303 let line_height = font_size.to_pixels(window.rem_size()) * 1.5;
304 let focus_handle = self.editor.focus_handle(cx);
305 let inline_context_picker = self.inline_context_picker.clone();
306 let bg_color = cx.theme().colors().editor_background;
307 let is_streaming_completion = self.thread.read(cx).is_streaming();
308 let button_width = px(64.);
309 let is_model_selected = self.is_model_selected(cx);
310 let is_editor_empty = self.is_editor_empty(cx);
311 let submit_label_color = if is_editor_empty {
312 Color::Muted
313 } else {
314 Color::Default
315 };
316
317 v_flex()
318 .key_context("MessageEditor")
319 .on_action(cx.listener(Self::chat))
320 .on_action(cx.listener(Self::toggle_model_selector))
321 .on_action(cx.listener(Self::toggle_context_picker))
322 .on_action(cx.listener(Self::remove_all_context))
323 .on_action(cx.listener(Self::move_up))
324 .on_action(cx.listener(Self::toggle_chat_mode))
325 .size_full()
326 .gap_2()
327 .p_2()
328 .bg(bg_color)
329 .child(self.context_strip.clone())
330 .child(
331 v_flex()
332 .gap_4()
333 .child({
334 let settings = ThemeSettings::get_global(cx);
335 let text_style = TextStyle {
336 color: cx.theme().colors().text,
337 font_family: settings.ui_font.family.clone(),
338 font_features: settings.ui_font.features.clone(),
339 font_size: font_size.into(),
340 font_weight: settings.ui_font.weight,
341 line_height: line_height.into(),
342 ..Default::default()
343 };
344
345 EditorElement::new(
346 &self.editor,
347 EditorStyle {
348 background: bg_color,
349 local_player: cx.theme().players().local(),
350 text: text_style,
351 ..Default::default()
352 },
353 )
354 })
355 .child(
356 PopoverMenu::new("inline-context-picker")
357 .menu(move |window, cx| {
358 inline_context_picker.update(cx, |this, cx| {
359 this.init(window, cx);
360 });
361
362 Some(inline_context_picker.clone())
363 })
364 .attach(gpui::Corner::TopLeft)
365 .anchor(gpui::Corner::BottomLeft)
366 .offset(gpui::Point {
367 x: px(0.0),
368 y: px(-ThemeSettings::clamp_font_size(
369 ThemeSettings::get_global(cx).ui_font_size,
370 )
371 .0 * 2.0)
372 - px(4.0),
373 })
374 .with_handle(self.inline_context_picker_menu_handle.clone()),
375 )
376 .child(
377 h_flex()
378 .justify_between()
379 .child(
380 Switch::new("use-tools", self.use_tools.into())
381 .label("Tools")
382 .on_click(cx.listener(|this, selection, _window, _cx| {
383 this.use_tools = match selection {
384 ToggleState::Selected => true,
385 ToggleState::Unselected
386 | ToggleState::Indeterminate => false,
387 };
388 }))
389 .key_binding(KeyBinding::for_action_in(
390 &ChatMode,
391 &focus_handle,
392 window,
393 )),
394 )
395 .child(h_flex().gap_1().child(self.model_selector.clone()).child(
396 if is_streaming_completion {
397 ButtonLike::new("cancel-generation")
398 .width(button_width.into())
399 .style(ButtonStyle::Tinted(TintColor::Accent))
400 .child(
401 h_flex()
402 .w_full()
403 .justify_between()
404 .child(
405 Label::new("Cancel")
406 .size(LabelSize::Small)
407 .with_animation(
408 "pulsating-label",
409 Animation::new(Duration::from_secs(2))
410 .repeat()
411 .with_easing(pulsating_between(
412 0.4, 0.8,
413 )),
414 |label, delta| label.alpha(delta),
415 ),
416 )
417 .children(
418 KeyBinding::for_action_in(
419 &editor::actions::Cancel,
420 &focus_handle,
421 window,
422 )
423 .map(|binding| binding.into_any_element()),
424 ),
425 )
426 .on_click(move |_event, window, cx| {
427 focus_handle.dispatch_action(
428 &editor::actions::Cancel,
429 window,
430 cx,
431 );
432 })
433 } else {
434 ButtonLike::new("submit-message")
435 .width(button_width.into())
436 .style(ButtonStyle::Filled)
437 .disabled(is_editor_empty || !is_model_selected)
438 .child(
439 h_flex()
440 .w_full()
441 .justify_between()
442 .child(
443 Label::new("Submit")
444 .size(LabelSize::Small)
445 .color(submit_label_color),
446 )
447 .children(
448 KeyBinding::for_action_in(
449 &Chat,
450 &focus_handle,
451 window,
452 )
453 .map(|binding| binding.into_any_element()),
454 ),
455 )
456 .on_click(move |_event, window, cx| {
457 focus_handle.dispatch_action(&Chat, window, cx);
458 })
459 .when(is_editor_empty, |button| {
460 button
461 .tooltip(Tooltip::text("Type a message to submit"))
462 })
463 .when(!is_model_selected, |button| {
464 button.tooltip(Tooltip::text(
465 "Select a model to continue",
466 ))
467 })
468 },
469 )),
470 ),
471 )
472 }
473}