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