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