assistant2.rs

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