assistant2.rs

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