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_1()
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| self.tool_registry.render_tool_call(tool_call, cx))
728                    .collect::<Vec<AnyElement>>();
729
730                let tools_body = if tools.is_empty() {
731                    None
732                } else {
733                    Some(div().children(tools).into_any_element())
734                };
735
736                div()
737                    .when(!is_last, |element| element.mb_2())
738                    .child(crate::ui::ChatMessage::new(
739                        *id,
740                        UserOrAssistant::Assistant,
741                        assistant_body,
742                        tools_body,
743                        self.is_message_collapsed(id),
744                        Box::new(cx.listener({
745                            let id = *id;
746                            move |assistant_chat, _event, _cx| {
747                                assistant_chat.toggle_message_collapsed(id)
748                            }
749                        })),
750                    ))
751                    .child(self.render_error(error.clone(), ix, cx))
752                    .into_any()
753            }
754        }
755    }
756
757    fn completion_messages(&self, cx: &mut WindowContext) -> Vec<CompletionMessage> {
758        let mut completion_messages = Vec::new();
759
760        for message in &self.messages {
761            match message {
762                ChatMessage::User(UserMessage {
763                    body, attachments, ..
764                }) => {
765                    completion_messages.extend(
766                        attachments
767                            .into_iter()
768                            .filter_map(|attachment| attachment.message.clone())
769                            .map(|content| CompletionMessage::System { content }),
770                    );
771
772                    // Show user's message last so that the assistant is grounded in the user's request
773                    completion_messages.push(CompletionMessage::User {
774                        content: body.read(cx).text(cx),
775                    });
776                }
777                ChatMessage::Assistant(AssistantMessage {
778                    body, tool_calls, ..
779                }) => {
780                    // In no case do we want to send an empty message. This shouldn't happen, but we might as well
781                    // not break the Chat API if it does.
782                    if body.text.is_empty() && tool_calls.is_empty() {
783                        continue;
784                    }
785
786                    let tool_calls_from_assistant = tool_calls
787                        .iter()
788                        .map(|tool_call| ToolCall {
789                            content: ToolCallContent::Function {
790                                function: FunctionContent {
791                                    name: tool_call.name.clone(),
792                                    arguments: tool_call.arguments.clone(),
793                                },
794                            },
795                            id: tool_call.id.clone(),
796                        })
797                        .collect();
798
799                    completion_messages.push(CompletionMessage::Assistant {
800                        content: Some(body.text.to_string()),
801                        tool_calls: tool_calls_from_assistant,
802                    });
803
804                    for tool_call in tool_calls {
805                        // Every tool call _must_ have a result by ID, otherwise OpenAI will error.
806                        let content = match &tool_call.result {
807                            Some(result) => result.format(&tool_call.name),
808                            None => "".to_string(),
809                        };
810
811                        completion_messages.push(CompletionMessage::Tool {
812                            content,
813                            tool_call_id: tool_call.id.clone(),
814                        });
815                    }
816                }
817            }
818        }
819
820        completion_messages
821    }
822}
823
824impl Render for AssistantChat {
825    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
826        div()
827            .relative()
828            .flex_1()
829            .v_flex()
830            .key_context("AssistantChat")
831            .on_action(cx.listener(Self::submit))
832            .on_action(cx.listener(Self::cancel))
833            .text_color(Color::Default.color(cx))
834            .child(list(self.list_state.clone()).flex_1())
835            .child(Composer::new(
836                self.composer_editor.clone(),
837                self.project_index_button.clone(),
838                self.active_file_button.clone(),
839                crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
840                    .into_any_element(),
841            ))
842    }
843}
844
845#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
846pub struct MessageId(usize);
847
848impl MessageId {
849    fn post_inc(&mut self) -> Self {
850        let id = *self;
851        self.0 += 1;
852        id
853    }
854}
855
856enum ChatMessage {
857    User(UserMessage),
858    Assistant(AssistantMessage),
859}
860
861impl ChatMessage {
862    fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
863        match self {
864            ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
865            ChatMessage::Assistant(_) => None,
866        }
867    }
868}
869
870struct UserMessage {
871    id: MessageId,
872    body: View<Editor>,
873    attachments: Vec<UserAttachment>,
874}
875
876struct AssistantMessage {
877    id: MessageId,
878    body: RichText,
879    tool_calls: Vec<ToolFunctionCall>,
880    error: Option<SharedString>,
881}