assistant2.rs

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