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