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}