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