1mod assistant_settings;
2mod attachments;
3mod completion_provider;
4mod saved_conversation;
5mod saved_conversation_picker;
6mod tools;
7pub mod ui;
8
9use crate::saved_conversation::{SavedConversation, SavedMessage, SavedMessageRole};
10use crate::saved_conversation_picker::SavedConversationPicker;
11use crate::{
12 attachments::ActiveEditorAttachmentTool,
13 tools::{CreateBufferTool, ProjectIndexTool},
14 ui::UserOrAssistant,
15};
16use ::ui::{div, prelude::*, Color, Tooltip, ViewContext};
17use anyhow::{Context, Result};
18use assistant_tooling::{
19 tool_running_placeholder, AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry,
20 UserAttachment,
21};
22use client::{proto, Client, UserStore};
23use collections::HashMap;
24use completion_provider::*;
25use editor::Editor;
26use feature_flags::FeatureFlagAppExt as _;
27use fs::Fs;
28use futures::{future::join_all, StreamExt};
29use gpui::{
30 list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
31 FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
32};
33use language::{language_settings::SoftWrap, LanguageRegistry};
34use open_ai::{FunctionContent, ToolCall, ToolCallContent};
35use rich_text::RichText;
36use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
37use serde::{Deserialize, Serialize};
38use settings::Settings;
39use std::sync::Arc;
40use tools::AnnotationTool;
41use ui::{ActiveFileButton, Composer, ProjectIndexButton};
42use util::paths::CONVERSATIONS_DIR;
43use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
44use workspace::{
45 dock::{DockPosition, Panel, PanelEvent},
46 Workspace,
47};
48
49pub use assistant_settings::AssistantSettings;
50
51const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
52
53#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
54pub struct Submit(SubmitMode);
55
56/// There are multiple different ways to submit a model request, represented by this enum.
57#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
58pub enum SubmitMode {
59 /// Only include the conversation.
60 Simple,
61 /// Send the current file as context.
62 CurrentFile,
63 /// Search the codebase and send relevant excerpts.
64 Codebase,
65}
66
67gpui::actions!(
68 assistant2,
69 [
70 Cancel,
71 ToggleFocus,
72 DebugProjectIndex,
73 ToggleSavedConversations
74 ]
75);
76gpui::impl_actions!(assistant2, [Submit]);
77
78pub fn init(client: Arc<Client>, cx: &mut AppContext) {
79 AssistantSettings::register(cx);
80
81 cx.spawn(|mut cx| {
82 let client = client.clone();
83 async move {
84 let embedding_provider = CloudEmbeddingProvider::new(client.clone());
85 let semantic_index = SemanticIndex::new(
86 EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
87 Arc::new(embedding_provider),
88 &mut cx,
89 )
90 .await?;
91 cx.update(|cx| cx.set_global(semantic_index))
92 }
93 })
94 .detach();
95
96 cx.set_global(CompletionProvider::new(CloudCompletionProvider::new(
97 client,
98 )));
99
100 cx.observe_new_views(
101 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
102 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
103 workspace.toggle_panel_focus::<AssistantPanel>(cx);
104 });
105 workspace.register_action(|workspace, _: &DebugProjectIndex, cx| {
106 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
107 let index = panel.read(cx).chat.read(cx).project_index.clone();
108 let view = cx.new_view(|cx| ProjectIndexDebugView::new(index, cx));
109 workspace.add_item_to_center(Box::new(view), cx);
110 }
111 });
112 },
113 )
114 .detach();
115 cx.observe_new_views(SavedConversationPicker::register)
116 .detach();
117}
118
119pub fn enabled(cx: &AppContext) -> bool {
120 cx.is_staff()
121}
122
123pub struct AssistantPanel {
124 chat: View<AssistantChat>,
125 width: Option<Pixels>,
126}
127
128impl AssistantPanel {
129 pub fn load(
130 workspace: WeakView<Workspace>,
131 cx: AsyncWindowContext,
132 ) -> Task<Result<View<Self>>> {
133 cx.spawn(|mut cx| async move {
134 let (app_state, project) = workspace.update(&mut cx, |workspace, _| {
135 (workspace.app_state().clone(), workspace.project().clone())
136 })?;
137
138 cx.new_view(|cx| {
139 let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
140 semantic_index.project_index(project.clone(), cx)
141 });
142
143 let mut tool_registry = ToolRegistry::new();
144 tool_registry
145 .register(ProjectIndexTool::new(project_index.clone()), cx)
146 .unwrap();
147 tool_registry
148 .register(
149 CreateBufferTool::new(workspace.clone(), project.clone()),
150 cx,
151 )
152 .unwrap();
153 tool_registry
154 .register(AnnotationTool::new(workspace.clone(), project.clone()), cx)
155 .unwrap();
156
157 let mut attachment_registry = AttachmentRegistry::new();
158 attachment_registry
159 .register(ActiveEditorAttachmentTool::new(workspace.clone(), cx));
160
161 Self::new(
162 project.read(cx).fs().clone(),
163 app_state.languages.clone(),
164 Arc::new(tool_registry),
165 Arc::new(attachment_registry),
166 app_state.user_store.clone(),
167 project_index,
168 workspace,
169 cx,
170 )
171 })
172 })
173 }
174
175 #[allow(clippy::too_many_arguments)]
176 pub fn new(
177 fs: Arc<dyn Fs>,
178 language_registry: Arc<LanguageRegistry>,
179 tool_registry: Arc<ToolRegistry>,
180 attachment_registry: Arc<AttachmentRegistry>,
181 user_store: Model<UserStore>,
182 project_index: Model<ProjectIndex>,
183 workspace: WeakView<Workspace>,
184 cx: &mut ViewContext<Self>,
185 ) -> Self {
186 let chat = cx.new_view(|cx| {
187 AssistantChat::new(
188 fs,
189 language_registry,
190 tool_registry.clone(),
191 attachment_registry,
192 user_store,
193 project_index,
194 workspace,
195 cx,
196 )
197 });
198
199 Self { width: None, chat }
200 }
201}
202
203impl Render for AssistantPanel {
204 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
205 div()
206 .size_full()
207 .v_flex()
208 .bg(cx.theme().colors().panel_background)
209 .child(self.chat.clone())
210 }
211}
212
213impl Panel for AssistantPanel {
214 fn persistent_name() -> &'static str {
215 "AssistantPanelv2"
216 }
217
218 fn position(&self, _cx: &WindowContext) -> workspace::dock::DockPosition {
219 // todo!("Add a setting / use assistant settings")
220 DockPosition::Right
221 }
222
223 fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool {
224 matches!(position, DockPosition::Right)
225 }
226
227 fn set_position(&mut self, _: workspace::dock::DockPosition, _: &mut ViewContext<Self>) {
228 // Do nothing until we have a setting for this
229 }
230
231 fn size(&self, _cx: &WindowContext) -> Pixels {
232 self.width.unwrap_or(px(400.))
233 }
234
235 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
236 self.width = size;
237 cx.notify();
238 }
239
240 fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
241 Some(IconName::ZedAssistant)
242 }
243
244 fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
245 Some("Assistant Panel ✨")
246 }
247
248 fn toggle_action(&self) -> Box<dyn gpui::Action> {
249 Box::new(ToggleFocus)
250 }
251}
252
253impl EventEmitter<PanelEvent> for AssistantPanel {}
254
255impl FocusableView for AssistantPanel {
256 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
257 self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
258 }
259}
260
261pub struct AssistantChat {
262 model: String,
263 messages: Vec<ChatMessage>,
264 list_state: ListState,
265 fs: Arc<dyn Fs>,
266 language_registry: Arc<LanguageRegistry>,
267 composer_editor: View<Editor>,
268 project_index_button: View<ProjectIndexButton>,
269 active_file_button: Option<View<ActiveFileButton>>,
270 user_store: Model<UserStore>,
271 next_message_id: MessageId,
272 collapsed_messages: HashMap<MessageId, bool>,
273 editing_message: Option<EditingMessage>,
274 pending_completion: Option<Task<()>>,
275 tool_registry: Arc<ToolRegistry>,
276 attachment_registry: Arc<AttachmentRegistry>,
277 project_index: Model<ProjectIndex>,
278}
279
280struct EditingMessage {
281 id: MessageId,
282 old_body: Arc<str>,
283 body: View<Editor>,
284}
285
286impl AssistantChat {
287 #[allow(clippy::too_many_arguments)]
288 fn new(
289 fs: Arc<dyn Fs>,
290 language_registry: Arc<LanguageRegistry>,
291 tool_registry: Arc<ToolRegistry>,
292 attachment_registry: Arc<AttachmentRegistry>,
293 user_store: Model<UserStore>,
294 project_index: Model<ProjectIndex>,
295 workspace: WeakView<Workspace>,
296 cx: &mut ViewContext<Self>,
297 ) -> Self {
298 let model = CompletionProvider::get(cx).default_model();
299 let view = cx.view().downgrade();
300 let list_state = ListState::new(
301 0,
302 ListAlignment::Bottom,
303 px(1024.),
304 move |ix, cx: &mut WindowContext| {
305 view.update(cx, |this, cx| this.render_message(ix, cx))
306 .unwrap()
307 },
308 );
309
310 let project_index_button = cx.new_view(|cx| {
311 ProjectIndexButton::new(project_index.clone(), tool_registry.clone(), cx)
312 });
313
314 let active_file_button = match workspace.upgrade() {
315 Some(workspace) => {
316 Some(cx.new_view(
317 |cx| ActiveFileButton::new(attachment_registry.clone(), workspace, cx), //
318 ))
319 }
320 _ => None,
321 };
322
323 Self {
324 model,
325 messages: Vec::new(),
326 composer_editor: cx.new_view(|cx| {
327 let mut editor = Editor::auto_height(80, cx);
328 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
329 editor.set_placeholder_text("Send a message…", cx);
330 editor
331 }),
332 list_state,
333 user_store,
334 fs,
335 language_registry,
336 project_index_button,
337 active_file_button,
338 project_index,
339 next_message_id: MessageId(0),
340 editing_message: None,
341 collapsed_messages: HashMap::default(),
342 pending_completion: None,
343 attachment_registry,
344 tool_registry,
345 }
346 }
347
348 fn editing_message_id(&self) -> Option<MessageId> {
349 self.editing_message.as_ref().map(|message| message.id)
350 }
351
352 fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
353 self.messages.iter().find_map(|message| match message {
354 ChatMessage::User(message) => message
355 .body
356 .focus_handle(cx)
357 .contains_focused(cx)
358 .then_some(message.id),
359 ChatMessage::Assistant(_) => None,
360 })
361 }
362
363 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
364 // If we're currently editing a message, cancel the edit.
365 if let Some(editing_message) = self.editing_message.take() {
366 editing_message
367 .body
368 .update(cx, |body, cx| body.set_text(editing_message.old_body, cx));
369 return;
370 }
371
372 if self.pending_completion.take().is_some() {
373 if let Some(ChatMessage::Assistant(grouping)) = self.messages.last() {
374 if grouping.messages.is_empty() {
375 self.pop_message(cx);
376 }
377 }
378 return;
379 }
380
381 cx.propagate();
382 }
383
384 fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
385 if let Some(focused_message_id) = self.focused_message_id(cx) {
386 self.truncate_messages(focused_message_id, cx);
387 self.pending_completion.take();
388 self.composer_editor.focus_handle(cx).focus(cx);
389 if self.editing_message_id() == Some(focused_message_id) {
390 self.editing_message.take();
391 }
392 } else if self.composer_editor.focus_handle(cx).is_focused(cx) {
393 // Don't allow multiple concurrent completions.
394 if self.pending_completion.is_some() {
395 cx.propagate();
396 return;
397 }
398
399 let message = self.composer_editor.update(cx, |composer_editor, cx| {
400 let text = composer_editor.text(cx);
401 let id = self.next_message_id.post_inc();
402 let body = cx.new_view(|cx| {
403 let mut editor = Editor::auto_height(80, cx);
404 editor.set_text(text, cx);
405 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
406 editor
407 });
408 composer_editor.clear(cx);
409
410 ChatMessage::User(UserMessage {
411 id,
412 body,
413 attachments: Vec::new(),
414 })
415 });
416 self.push_message(message, cx);
417 } else {
418 log::error!("unexpected state: no user message editor is focused.");
419 return;
420 }
421
422 let mode = *mode;
423 self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
424 let attachments_task = this.update(&mut cx, |this, cx| {
425 let attachment_registry = this.attachment_registry.clone();
426 attachment_registry.call_all_attachment_tools(cx)
427 });
428
429 let attachments = maybe!(async {
430 let attachments_task = attachments_task?;
431 let attachments = attachments_task.await?;
432
433 anyhow::Ok(attachments)
434 })
435 .await
436 .log_err()
437 .unwrap_or_default();
438
439 // Set the attachments to the _last_ user message
440 this.update(&mut cx, |this, _cx| {
441 if let Some(ChatMessage::User(message)) = this.messages.last_mut() {
442 message.attachments = attachments;
443 }
444 })
445 .log_err();
446
447 Self::request_completion(
448 this.clone(),
449 mode,
450 MAX_COMPLETION_CALLS_PER_SUBMISSION,
451 &mut cx,
452 )
453 .await
454 .log_err();
455
456 this.update(&mut cx, |this, _cx| {
457 this.pending_completion = None;
458 })
459 .context("Failed to push new user message")
460 .log_err();
461 }));
462 }
463
464 async fn request_completion(
465 this: WeakView<Self>,
466 mode: SubmitMode,
467 limit: usize,
468 cx: &mut AsyncWindowContext,
469 ) -> Result<()> {
470 let mut call_count = 0;
471 loop {
472 let complete = async {
473 let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| {
474 this.push_new_assistant_message(cx);
475
476 let definitions = if call_count < limit
477 && matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
478 {
479 this.tool_registry.definitions()
480 } else {
481 Vec::new()
482 };
483 call_count += 1;
484
485 (
486 definitions,
487 this.model.clone(),
488 this.completion_messages(cx),
489 )
490 })?;
491
492 let messages = messages.await?;
493
494 let completion = cx.update(|cx| {
495 CompletionProvider::get(cx).complete(
496 model_name,
497 messages,
498 Vec::new(),
499 1.0,
500 tool_definitions,
501 )
502 });
503
504 let mut stream = completion?.await?;
505 let mut body = String::new();
506 while let Some(delta) = stream.next().await {
507 let delta = delta?;
508 this.update(cx, |this, cx| {
509 if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
510 messages,
511 ..
512 })) = this.messages.last_mut()
513 {
514 if messages.is_empty() {
515 messages.push(AssistantMessage {
516 body: RichText::default(),
517 tool_calls: Vec::new(),
518 })
519 }
520
521 let message = messages.last_mut().unwrap();
522
523 if let Some(content) = &delta.content {
524 body.push_str(content);
525 }
526
527 for tool_call in delta.tool_calls {
528 let index = tool_call.index as usize;
529 if index >= message.tool_calls.len() {
530 message.tool_calls.resize_with(index + 1, Default::default);
531 }
532 let call = &mut message.tool_calls[index];
533
534 if let Some(id) = &tool_call.id {
535 call.id.push_str(id);
536 }
537
538 match tool_call.variant {
539 Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
540 if let Some(name) = &tool_call.name {
541 call.name.push_str(name);
542 }
543 if let Some(arguments) = &tool_call.arguments {
544 call.arguments.push_str(arguments);
545 }
546 }
547 None => {}
548 }
549 }
550
551 message.body =
552 RichText::new(body.clone(), &[], &this.language_registry);
553 cx.notify();
554 } else {
555 unreachable!()
556 }
557 })?;
558 }
559
560 anyhow::Ok(())
561 }
562 .await;
563
564 let mut tool_tasks = Vec::new();
565 this.update(cx, |this, cx| {
566 if let Some(ChatMessage::Assistant(GroupedAssistantMessage {
567 error: message_error,
568 messages,
569 ..
570 })) = this.messages.last_mut()
571 {
572 if let Err(error) = complete {
573 message_error.replace(SharedString::from(error.to_string()));
574 cx.notify();
575 } else {
576 if let Some(current_message) = messages.last_mut() {
577 for tool_call in current_message.tool_calls.iter() {
578 tool_tasks.push(this.tool_registry.call(tool_call, cx));
579 }
580 }
581 }
582 }
583 })?;
584
585 if tool_tasks.is_empty() {
586 return Ok(());
587 }
588
589 let tools = join_all(tool_tasks.into_iter()).await;
590 // If the WindowContext went away for any tool's view we don't include it
591 // especially since the below call would fail for the same reason.
592 let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
593
594 this.update(cx, |this, cx| {
595 if let Some(ChatMessage::Assistant(GroupedAssistantMessage { messages, .. })) =
596 this.messages.last_mut()
597 {
598 if let Some(current_message) = messages.last_mut() {
599 current_message.tool_calls = tools;
600 cx.notify();
601 } else {
602 unreachable!()
603 }
604 }
605 })?;
606 }
607 }
608
609 fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
610 // If the last message is a grouped assistant message, add to the grouped message
611 if let Some(ChatMessage::Assistant(GroupedAssistantMessage { messages, .. })) =
612 self.messages.last_mut()
613 {
614 messages.push(AssistantMessage {
615 body: RichText::default(),
616 tool_calls: Vec::new(),
617 });
618 return;
619 }
620
621 let message = ChatMessage::Assistant(GroupedAssistantMessage {
622 id: self.next_message_id.post_inc(),
623 messages: vec![AssistantMessage {
624 body: RichText::default(),
625 tool_calls: Vec::new(),
626 }],
627 error: None,
628 });
629 self.push_message(message, cx);
630 }
631
632 fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
633 let old_len = self.messages.len();
634 let focus_handle = Some(message.focus_handle(cx));
635 self.messages.push(message);
636 self.list_state
637 .splice_focusable(old_len..old_len, focus_handle);
638 cx.notify();
639 }
640
641 fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
642 if self.messages.is_empty() {
643 return;
644 }
645
646 self.messages.pop();
647 self.list_state
648 .splice(self.messages.len()..self.messages.len() + 1, 0);
649 cx.notify();
650 }
651
652 fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
653 if let Some(index) = self.messages.iter().position(|message| match message {
654 ChatMessage::User(message) => message.id == last_message_id,
655 ChatMessage::Assistant(message) => message.id == last_message_id,
656 }) {
657 self.list_state.splice(index + 1..self.messages.len(), 0);
658 self.messages.truncate(index + 1);
659 cx.notify();
660 }
661 }
662
663 fn is_message_collapsed(&self, id: &MessageId) -> bool {
664 self.collapsed_messages.get(id).copied().unwrap_or_default()
665 }
666
667 fn toggle_message_collapsed(&mut self, id: MessageId) {
668 let entry = self.collapsed_messages.entry(id).or_insert(false);
669 *entry = !*entry;
670 }
671
672 fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
673 let messages = self
674 .messages
675 .drain(..)
676 .map(|message| {
677 let text = match &message {
678 ChatMessage::User(message) => message.body.read(cx).text(cx),
679 ChatMessage::Assistant(message) => message
680 .messages
681 .iter()
682 .map(|message| message.body.text.to_string())
683 .collect::<Vec<_>>()
684 .join("\n\n"),
685 };
686
687 SavedMessage {
688 id: message.id(),
689 role: match message {
690 ChatMessage::User(_) => SavedMessageRole::User,
691 ChatMessage::Assistant(_) => SavedMessageRole::Assistant,
692 },
693 text,
694 }
695 })
696 .collect::<Vec<_>>();
697
698 // Reset the chat for the new conversation.
699 self.list_state.reset(0);
700 self.editing_message.take();
701 self.collapsed_messages.clear();
702
703 let title = messages
704 .first()
705 .map(|message| message.text.clone())
706 .unwrap_or_else(|| "A conversation with the assistant.".to_string());
707
708 let saved_conversation = SavedConversation {
709 version: "0.3.0".to_string(),
710 title,
711 messages,
712 };
713
714 let discriminant = 1;
715
716 let path = CONVERSATIONS_DIR.join(&format!(
717 "{title} - {discriminant}.zed.{version}.json",
718 title = saved_conversation.title,
719 version = saved_conversation.version
720 ));
721
722 cx.spawn({
723 let fs = self.fs.clone();
724 |_this, _cx| async move {
725 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
726 fs.atomic_write(path, serde_json::to_string(&saved_conversation)?)
727 .await?;
728
729 anyhow::Ok(())
730 }
731 })
732 .detach_and_log_err(cx);
733 }
734
735 fn render_error(
736 &self,
737 error: Option<SharedString>,
738 _ix: usize,
739 cx: &mut ViewContext<Self>,
740 ) -> AnyElement {
741 let theme = cx.theme();
742
743 if let Some(error) = error {
744 div()
745 .py_1()
746 .px_2()
747 .mx_neg_1()
748 .rounded_md()
749 .border_1()
750 .border_color(theme.status().error_border)
751 // .bg(theme.status().error_background)
752 .text_color(theme.status().error)
753 .child(error.clone())
754 .into_any_element()
755 } else {
756 div().into_any_element()
757 }
758 }
759
760 fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
761 let is_first = ix == 0;
762 let is_last = ix == self.messages.len().saturating_sub(1);
763
764 let padding = Spacing::Large.rems(cx);
765
766 // Whenever there's a run of assistant messages, group as one Assistant UI element
767
768 match &self.messages[ix] {
769 ChatMessage::User(UserMessage {
770 id,
771 body,
772 attachments,
773 }) => div()
774 .id(SharedString::from(format!("message-{}-container", id.0)))
775 .when(is_first, |this| this.pt(padding))
776 .map(|element| {
777 if self.editing_message_id() == Some(*id) {
778 element.child(Composer::new(
779 body.clone(),
780 self.project_index_button.clone(),
781 self.active_file_button.clone(),
782 crate::ui::ModelSelector::new(
783 cx.view().downgrade(),
784 self.model.clone(),
785 )
786 .into_any_element(),
787 ))
788 } else {
789 element
790 .on_click(cx.listener({
791 let id = *id;
792 let body = body.clone();
793 move |assistant_chat, event: &ClickEvent, cx| {
794 if event.up.click_count == 2 {
795 assistant_chat.editing_message = Some(EditingMessage {
796 id,
797 body: body.clone(),
798 old_body: body.read(cx).text(cx).into(),
799 });
800 body.focus_handle(cx).focus(cx);
801 }
802 }
803 }))
804 .child(
805 crate::ui::ChatMessage::new(
806 *id,
807 UserOrAssistant::User(self.user_store.read(cx).current_user()),
808 // todo!(): clean up the vec usage
809 vec![
810 RichText::new(
811 body.read(cx).text(cx),
812 &[],
813 &self.language_registry,
814 )
815 .element(ElementId::from(id.0), cx),
816 h_flex()
817 .gap_2()
818 .children(
819 attachments
820 .iter()
821 .map(|attachment| attachment.view.clone()),
822 )
823 .into_any_element(),
824 ],
825 self.is_message_collapsed(id),
826 Box::new(cx.listener({
827 let id = *id;
828 move |assistant_chat, _event, _cx| {
829 assistant_chat.toggle_message_collapsed(id)
830 }
831 })),
832 )
833 // TODO: Wire up selections.
834 .selected(is_last),
835 )
836 }
837 })
838 .into_any(),
839 ChatMessage::Assistant(GroupedAssistantMessage {
840 id,
841 messages,
842 error,
843 ..
844 }) => {
845 let mut message_elements = Vec::new();
846
847 for message in messages {
848 if !message.body.text.is_empty() {
849 message_elements.push(
850 div()
851 // todo!(): The element Id will need to be a combo of the base ID and the index within the grouping
852 .child(message.body.element(ElementId::from(id.0), cx))
853 .into_any_element(),
854 )
855 }
856
857 let tools = message
858 .tool_calls
859 .iter()
860 .map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
861 .collect::<Vec<AnyElement>>();
862
863 if !tools.is_empty() {
864 message_elements.push(div().children(tools).into_any_element())
865 }
866 }
867
868 if message_elements.is_empty() {
869 message_elements.push(tool_running_placeholder());
870 }
871
872 div()
873 .when(is_first, |this| this.pt(padding))
874 .child(
875 crate::ui::ChatMessage::new(
876 *id,
877 UserOrAssistant::Assistant,
878 message_elements,
879 self.is_message_collapsed(id),
880 Box::new(cx.listener({
881 let id = *id;
882 move |assistant_chat, _event, _cx| {
883 assistant_chat.toggle_message_collapsed(id)
884 }
885 })),
886 )
887 // TODO: Wire up selections.
888 .selected(is_last),
889 )
890 .child(self.render_error(error.clone(), ix, cx))
891 .into_any()
892 }
893 }
894 }
895
896 fn completion_messages(&self, cx: &mut WindowContext) -> Task<Result<Vec<CompletionMessage>>> {
897 let project_index = self.project_index.read(cx);
898 let project = project_index.project();
899 let fs = project_index.fs();
900
901 let mut project_context = ProjectContext::new(project, fs);
902 let mut completion_messages = Vec::new();
903
904 for message in &self.messages {
905 match message {
906 ChatMessage::User(UserMessage {
907 body, attachments, ..
908 }) => {
909 for attachment in attachments {
910 if let Some(content) = attachment.generate(&mut project_context, cx) {
911 completion_messages.push(CompletionMessage::System { content });
912 }
913 }
914
915 // Show user's message last so that the assistant is grounded in the user's request
916 completion_messages.push(CompletionMessage::User {
917 content: body.read(cx).text(cx),
918 });
919 }
920 ChatMessage::Assistant(GroupedAssistantMessage { messages, .. }) => {
921 for message in messages {
922 let body = message.body.clone();
923
924 if body.text.is_empty() && message.tool_calls.is_empty() {
925 continue;
926 }
927
928 let tool_calls_from_assistant = message
929 .tool_calls
930 .iter()
931 .map(|tool_call| ToolCall {
932 content: ToolCallContent::Function {
933 function: FunctionContent {
934 name: tool_call.name.clone(),
935 arguments: tool_call.arguments.clone(),
936 },
937 },
938 id: tool_call.id.clone(),
939 })
940 .collect();
941
942 completion_messages.push(CompletionMessage::Assistant {
943 content: Some(body.text.to_string()),
944 tool_calls: tool_calls_from_assistant,
945 });
946
947 for tool_call in &message.tool_calls {
948 // Every tool call _must_ have a result by ID, otherwise OpenAI will error.
949 let content = match &tool_call.result {
950 Some(result) => {
951 result.generate(&tool_call.name, &mut project_context, cx)
952 }
953 None => "".to_string(),
954 };
955
956 completion_messages.push(CompletionMessage::Tool {
957 content,
958 tool_call_id: tool_call.id.clone(),
959 });
960 }
961 }
962 }
963 }
964 }
965
966 let system_message = project_context.generate_system_message(cx);
967
968 cx.background_executor().spawn(async move {
969 let content = system_message.await?;
970 completion_messages.insert(0, CompletionMessage::System { content });
971 Ok(completion_messages)
972 })
973 }
974}
975
976impl Render for AssistantChat {
977 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
978 div()
979 .relative()
980 .flex_1()
981 .v_flex()
982 .key_context("AssistantChat")
983 .on_action(cx.listener(Self::submit))
984 .on_action(cx.listener(Self::cancel))
985 .text_color(Color::Default.color(cx))
986 .child(
987 h_flex()
988 .gap_2()
989 .child(
990 Button::new("open-saved-conversations", "Saved Conversations").on_click(
991 |_event, cx| cx.dispatch_action(Box::new(ToggleSavedConversations)),
992 ),
993 )
994 .child(
995 IconButton::new("new-conversation", IconName::Plus)
996 .on_click(cx.listener(move |this, _event, cx| {
997 this.new_conversation(cx);
998 }))
999 .tooltip(move |cx| Tooltip::text("New Conversation", cx)),
1000 ),
1001 )
1002 .child(list(self.list_state.clone()).flex_1())
1003 .child(Composer::new(
1004 self.composer_editor.clone(),
1005 self.project_index_button.clone(),
1006 self.active_file_button.clone(),
1007 crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
1008 .into_any_element(),
1009 ))
1010 }
1011}
1012
1013#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
1014pub struct MessageId(usize);
1015
1016impl MessageId {
1017 fn post_inc(&mut self) -> Self {
1018 let id = *self;
1019 self.0 += 1;
1020 id
1021 }
1022}
1023
1024enum ChatMessage {
1025 User(UserMessage),
1026 Assistant(GroupedAssistantMessage),
1027}
1028
1029impl ChatMessage {
1030 pub fn id(&self) -> MessageId {
1031 match self {
1032 ChatMessage::User(message) => message.id,
1033 ChatMessage::Assistant(message) => message.id,
1034 }
1035 }
1036
1037 fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
1038 match self {
1039 ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
1040 ChatMessage::Assistant(_) => None,
1041 }
1042 }
1043}
1044
1045struct UserMessage {
1046 id: MessageId,
1047 body: View<Editor>,
1048 attachments: Vec<UserAttachment>,
1049}
1050
1051struct AssistantMessage {
1052 body: RichText,
1053 tool_calls: Vec<ToolFunctionCall>,
1054}
1055
1056struct GroupedAssistantMessage {
1057 id: MessageId,
1058 messages: Vec<AssistantMessage>,
1059 error: Option<SharedString>,
1060}