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