assistant2.rs

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