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(message)) = self.messages.last() {
346 if message.body.text.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(AssistantMessage {
482 body: message_body,
483 tool_calls: message_tool_calls,
484 ..
485 })) = this.messages.last_mut()
486 {
487 if let Some(content) = &delta.content {
488 body.push_str(content);
489 }
490
491 for tool_call in delta.tool_calls {
492 let index = tool_call.index as usize;
493 if index >= message_tool_calls.len() {
494 message_tool_calls.resize_with(index + 1, Default::default);
495 }
496 let call = &mut message_tool_calls[index];
497
498 if let Some(id) = &tool_call.id {
499 call.id.push_str(id);
500 }
501
502 match tool_call.variant {
503 Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
504 if let Some(name) = &tool_call.name {
505 call.name.push_str(name);
506 }
507 if let Some(arguments) = &tool_call.arguments {
508 call.arguments.push_str(arguments);
509 }
510 }
511 None => {}
512 }
513 }
514
515 *message_body =
516 RichText::new(body.clone(), &[], &this.language_registry);
517 cx.notify();
518 } else {
519 unreachable!()
520 }
521 })?;
522 }
523
524 anyhow::Ok(())
525 }
526 .await;
527
528 let mut tool_tasks = Vec::new();
529 this.update(cx, |this, cx| {
530 if let Some(ChatMessage::Assistant(AssistantMessage {
531 error: message_error,
532 tool_calls,
533 ..
534 })) = this.messages.last_mut()
535 {
536 if let Err(error) = complete {
537 message_error.replace(SharedString::from(error.to_string()));
538 cx.notify();
539 } else {
540 for tool_call in tool_calls.iter() {
541 tool_tasks.push(this.tool_registry.call(tool_call, cx));
542 }
543 }
544 }
545 })?;
546
547 if tool_tasks.is_empty() {
548 return Ok(());
549 }
550
551 let tools = join_all(tool_tasks.into_iter()).await;
552 // If the WindowContext went away for any tool's view we don't include it
553 // especially since the below call would fail for the same reason.
554 let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
555
556 this.update(cx, |this, cx| {
557 if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
558 this.messages.last_mut()
559 {
560 *tool_calls = tools;
561 cx.notify();
562 }
563 })?;
564 }
565 }
566
567 fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
568 let message = ChatMessage::Assistant(AssistantMessage {
569 id: self.next_message_id.post_inc(),
570 body: RichText::default(),
571 tool_calls: Vec::new(),
572 error: None,
573 });
574 self.push_message(message, cx);
575 }
576
577 fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
578 let old_len = self.messages.len();
579 let focus_handle = Some(message.focus_handle(cx));
580 self.messages.push(message);
581 self.list_state
582 .splice_focusable(old_len..old_len, focus_handle);
583 cx.notify();
584 }
585
586 fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
587 if self.messages.is_empty() {
588 return;
589 }
590
591 self.messages.pop();
592 self.list_state
593 .splice(self.messages.len()..self.messages.len() + 1, 0);
594 cx.notify();
595 }
596
597 fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
598 if let Some(index) = self.messages.iter().position(|message| match message {
599 ChatMessage::User(message) => message.id == last_message_id,
600 ChatMessage::Assistant(message) => message.id == last_message_id,
601 }) {
602 self.list_state.splice(index + 1..self.messages.len(), 0);
603 self.messages.truncate(index + 1);
604 cx.notify();
605 }
606 }
607
608 fn is_message_collapsed(&self, id: &MessageId) -> bool {
609 self.collapsed_messages.get(id).copied().unwrap_or_default()
610 }
611
612 fn toggle_message_collapsed(&mut self, id: MessageId) {
613 let entry = self.collapsed_messages.entry(id).or_insert(false);
614 *entry = !*entry;
615 }
616
617 fn render_error(
618 &self,
619 error: Option<SharedString>,
620 _ix: usize,
621 cx: &mut ViewContext<Self>,
622 ) -> AnyElement {
623 let theme = cx.theme();
624
625 if let Some(error) = error {
626 div()
627 .py_1()
628 .px_2()
629 .mx_neg_1()
630 .rounded_md()
631 .border_1()
632 .border_color(theme.status().error_border)
633 // .bg(theme.status().error_background)
634 .text_color(theme.status().error)
635 .child(error.clone())
636 .into_any_element()
637 } else {
638 div().into_any_element()
639 }
640 }
641
642 fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
643 let is_first = ix == 0;
644 let is_last = ix == self.messages.len() - 1;
645
646 let padding = Spacing::Large.rems(cx);
647
648 // Whenever there's a run of assistant messages, group as one Assistant UI element
649
650 match &self.messages[ix] {
651 ChatMessage::User(UserMessage {
652 id,
653 body,
654 attachments,
655 }) => div()
656 .id(SharedString::from(format!("message-{}-container", id.0)))
657 .when(is_first, |this| this.pt(padding))
658 .map(|element| {
659 if self.editing_message_id() == Some(*id) {
660 element.child(Composer::new(
661 body.clone(),
662 self.project_index_button.clone(),
663 self.active_file_button.clone(),
664 crate::ui::ModelSelector::new(
665 cx.view().downgrade(),
666 self.model.clone(),
667 )
668 .into_any_element(),
669 ))
670 } else {
671 element
672 .on_click(cx.listener({
673 let id = *id;
674 let body = body.clone();
675 move |assistant_chat, event: &ClickEvent, cx| {
676 if event.up.click_count == 2 {
677 assistant_chat.editing_message = Some(EditingMessage {
678 id,
679 body: body.clone(),
680 old_body: body.read(cx).text(cx).into(),
681 });
682 body.focus_handle(cx).focus(cx);
683 }
684 }
685 }))
686 .child(
687 crate::ui::ChatMessage::new(
688 *id,
689 UserOrAssistant::User(self.user_store.read(cx).current_user()),
690 Some(
691 RichText::new(
692 body.read(cx).text(cx),
693 &[],
694 &self.language_registry,
695 )
696 .element(ElementId::from(id.0), cx),
697 ),
698 Some(
699 h_flex()
700 .gap_2()
701 .children(
702 attachments
703 .iter()
704 .map(|attachment| attachment.view.clone()),
705 )
706 .into_any_element(),
707 ),
708 self.is_message_collapsed(id),
709 Box::new(cx.listener({
710 let id = *id;
711 move |assistant_chat, _event, _cx| {
712 assistant_chat.toggle_message_collapsed(id)
713 }
714 })),
715 )
716 // TODO: Wire up selections.
717 .selected(is_last),
718 )
719 }
720 })
721 .into_any(),
722 ChatMessage::Assistant(AssistantMessage {
723 id,
724 body,
725 error,
726 tool_calls,
727 ..
728 }) => {
729 let assistant_body = if body.text.is_empty() {
730 None
731 } else {
732 Some(
733 div()
734 .child(body.element(ElementId::from(id.0), cx))
735 .into_any_element(),
736 )
737 };
738
739 let tools = tool_calls
740 .iter()
741 .map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
742 .collect::<Vec<AnyElement>>();
743
744 let tools_body = if tools.is_empty() {
745 None
746 } else {
747 Some(div().children(tools).into_any_element())
748 };
749
750 div()
751 .when(is_first, |this| this.pt(padding))
752 .child(
753 crate::ui::ChatMessage::new(
754 *id,
755 UserOrAssistant::Assistant,
756 assistant_body,
757 tools_body,
758 self.is_message_collapsed(id),
759 Box::new(cx.listener({
760 let id = *id;
761 move |assistant_chat, _event, _cx| {
762 assistant_chat.toggle_message_collapsed(id)
763 }
764 })),
765 )
766 // TODO: Wire up selections.
767 .selected(is_last),
768 )
769 .child(self.render_error(error.clone(), ix, cx))
770 .into_any()
771 }
772 }
773 }
774
775 fn completion_messages(&self, cx: &mut WindowContext) -> Task<Result<Vec<CompletionMessage>>> {
776 let project_index = self.project_index.read(cx);
777 let project = project_index.project();
778 let fs = project_index.fs();
779
780 let mut project_context = ProjectContext::new(project, fs);
781 let mut completion_messages = Vec::new();
782
783 for message in &self.messages {
784 match message {
785 ChatMessage::User(UserMessage {
786 body, attachments, ..
787 }) => {
788 for attachment in attachments {
789 if let Some(content) = attachment.generate(&mut project_context, cx) {
790 completion_messages.push(CompletionMessage::System { content });
791 }
792 }
793
794 // Show user's message last so that the assistant is grounded in the user's request
795 completion_messages.push(CompletionMessage::User {
796 content: body.read(cx).text(cx),
797 });
798 }
799 ChatMessage::Assistant(AssistantMessage {
800 body, tool_calls, ..
801 }) => {
802 // In no case do we want to send an empty message. This shouldn't happen, but we might as well
803 // not break the Chat API if it does.
804 if body.text.is_empty() && tool_calls.is_empty() {
805 continue;
806 }
807
808 let tool_calls_from_assistant = tool_calls
809 .iter()
810 .map(|tool_call| ToolCall {
811 content: ToolCallContent::Function {
812 function: FunctionContent {
813 name: tool_call.name.clone(),
814 arguments: tool_call.arguments.clone(),
815 },
816 },
817 id: tool_call.id.clone(),
818 })
819 .collect();
820
821 completion_messages.push(CompletionMessage::Assistant {
822 content: Some(body.text.to_string()),
823 tool_calls: tool_calls_from_assistant,
824 });
825
826 for tool_call in tool_calls {
827 // Every tool call _must_ have a result by ID, otherwise OpenAI will error.
828 let content = match &tool_call.result {
829 Some(result) => {
830 result.generate(&tool_call.name, &mut project_context, cx)
831 }
832 None => "".to_string(),
833 };
834
835 completion_messages.push(CompletionMessage::Tool {
836 content,
837 tool_call_id: tool_call.id.clone(),
838 });
839 }
840 }
841 }
842 }
843
844 let system_message = project_context.generate_system_message(cx);
845
846 cx.background_executor().spawn(async move {
847 let content = system_message.await?;
848 completion_messages.insert(0, CompletionMessage::System { content });
849 Ok(completion_messages)
850 })
851 }
852}
853
854impl Render for AssistantChat {
855 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
856 div()
857 .relative()
858 .flex_1()
859 .v_flex()
860 .key_context("AssistantChat")
861 .on_action(cx.listener(Self::submit))
862 .on_action(cx.listener(Self::cancel))
863 .text_color(Color::Default.color(cx))
864 .child(list(self.list_state.clone()).flex_1())
865 .child(Composer::new(
866 self.composer_editor.clone(),
867 self.project_index_button.clone(),
868 self.active_file_button.clone(),
869 crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
870 .into_any_element(),
871 ))
872 }
873}
874
875#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
876pub struct MessageId(usize);
877
878impl MessageId {
879 fn post_inc(&mut self) -> Self {
880 let id = *self;
881 self.0 += 1;
882 id
883 }
884}
885
886enum ChatMessage {
887 User(UserMessage),
888 Assistant(AssistantMessage),
889}
890
891impl ChatMessage {
892 fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
893 match self {
894 ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
895 ChatMessage::Assistant(_) => None,
896 }
897 }
898}
899
900struct UserMessage {
901 id: MessageId,
902 body: View<Editor>,
903 attachments: Vec<UserAttachment>,
904}
905
906struct AssistantMessage {
907 id: MessageId,
908 body: RichText,
909 tool_calls: Vec<ToolFunctionCall>,
910 error: Option<SharedString>,
911}