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, ProjectIndexButton};
 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::ZedAssistant)
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    project_index_button: Option<View<ProjectIndexButton>>,
232    user_store: Model<UserStore>,
233    next_message_id: MessageId,
234    collapsed_messages: HashMap<MessageId, bool>,
235    editing_message: Option<EditingMessage>,
236    pending_completion: Option<Task<()>>,
237    tool_registry: Arc<ToolRegistry>,
238    project_index: Option<Model<ProjectIndex>>,
239}
240
241struct EditingMessage {
242    id: MessageId,
243    old_body: Arc<str>,
244    body: View<Editor>,
245}
246
247impl AssistantChat {
248    fn new(
249        language_registry: Arc<LanguageRegistry>,
250        tool_registry: Arc<ToolRegistry>,
251        user_store: Model<UserStore>,
252        project_index: Option<Model<ProjectIndex>>,
253        cx: &mut ViewContext<Self>,
254    ) -> Self {
255        let model = CompletionProvider::get(cx).default_model();
256        let view = cx.view().downgrade();
257        let list_state = ListState::new(
258            0,
259            ListAlignment::Bottom,
260            px(1024.),
261            move |ix, cx: &mut WindowContext| {
262                view.update(cx, |this, cx| this.render_message(ix, cx))
263                    .unwrap()
264            },
265        );
266
267        let project_index_button = project_index.clone().map(|project_index| {
268            cx.new_view(|cx| ProjectIndexButton::new(project_index, tool_registry.clone(), cx))
269        });
270
271        Self {
272            model,
273            messages: Vec::new(),
274            composer_editor: cx.new_view(|cx| {
275                let mut editor = Editor::auto_height(80, cx);
276                editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
277                editor.set_placeholder_text("Send a message…", cx);
278                editor
279            }),
280            list_state,
281            user_store,
282            language_registry,
283            project_index_button,
284            project_index,
285            next_message_id: MessageId(0),
286            editing_message: None,
287            collapsed_messages: HashMap::default(),
288            pending_completion: None,
289            tool_registry,
290        }
291    }
292
293    fn editing_message_id(&self) -> Option<MessageId> {
294        self.editing_message.as_ref().map(|message| message.id)
295    }
296
297    fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
298        self.messages.iter().find_map(|message| match message {
299            ChatMessage::User(message) => message
300                .body
301                .focus_handle(cx)
302                .contains_focused(cx)
303                .then_some(message.id),
304            ChatMessage::Assistant(_) => None,
305        })
306    }
307
308    fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
309        // If we're currently editing a message, cancel the edit.
310        if let Some(editing_message) = self.editing_message.take() {
311            editing_message
312                .body
313                .update(cx, |body, cx| body.set_text(editing_message.old_body, cx));
314            return;
315        }
316
317        if self.pending_completion.take().is_some() {
318            if let Some(ChatMessage::Assistant(message)) = self.messages.last() {
319                if message.body.text.is_empty() {
320                    self.pop_message(cx);
321                }
322            }
323            return;
324        }
325
326        cx.propagate();
327    }
328
329    fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
330        if let Some(focused_message_id) = self.focused_message_id(cx) {
331            self.truncate_messages(focused_message_id, cx);
332            self.pending_completion.take();
333            self.composer_editor.focus_handle(cx).focus(cx);
334            if self.editing_message_id() == Some(focused_message_id) {
335                self.editing_message.take();
336            }
337        } else if self.composer_editor.focus_handle(cx).is_focused(cx) {
338            // Don't allow multiple concurrent completions.
339            if self.pending_completion.is_some() {
340                cx.propagate();
341                return;
342            }
343
344            let message = self.composer_editor.update(cx, |composer_editor, cx| {
345                let text = composer_editor.text(cx);
346                let id = self.next_message_id.post_inc();
347                let body = cx.new_view(|cx| {
348                    let mut editor = Editor::auto_height(80, cx);
349                    editor.set_text(text, cx);
350                    editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
351                    editor
352                });
353                composer_editor.clear(cx);
354                ChatMessage::User(UserMessage { id, body })
355            });
356            self.push_message(message, cx);
357        } else {
358            log::error!("unexpected state: no user message editor is focused.");
359            return;
360        }
361
362        let mode = *mode;
363        self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
364            Self::request_completion(
365                this.clone(),
366                mode,
367                MAX_COMPLETION_CALLS_PER_SUBMISSION,
368                &mut cx,
369            )
370            .await
371            .log_err();
372
373            this.update(&mut cx, |this, _cx| {
374                this.pending_completion = None;
375            })
376            .context("Failed to push new user message")
377            .log_err();
378        }));
379    }
380
381    fn debug_project_index(&mut self, _: &DebugProjectIndex, cx: &mut ViewContext<Self>) {
382        if let Some(index) = &self.project_index {
383            index.update(cx, |project_index, cx| {
384                project_index.debug(cx).detach_and_log_err(cx)
385            });
386        }
387    }
388
389    async fn request_completion(
390        this: WeakView<Self>,
391        mode: SubmitMode,
392        limit: usize,
393        cx: &mut AsyncWindowContext,
394    ) -> Result<()> {
395        let mut call_count = 0;
396        loop {
397            let complete = async {
398                let completion = this.update(cx, |this, cx| {
399                    this.push_new_assistant_message(cx);
400
401                    let definitions = if call_count < limit
402                        && matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
403                    {
404                        this.tool_registry.definitions()
405                    } else {
406                        Vec::new()
407                    };
408                    call_count += 1;
409
410                    let messages = this.completion_messages(cx);
411
412                    CompletionProvider::get(cx).complete(
413                        this.model.clone(),
414                        messages,
415                        Vec::new(),
416                        1.0,
417                        definitions,
418                    )
419                });
420
421                let mut stream = completion?.await?;
422                let mut body = String::new();
423                while let Some(delta) = stream.next().await {
424                    let delta = delta?;
425                    this.update(cx, |this, cx| {
426                        if let Some(ChatMessage::Assistant(AssistantMessage {
427                            body: message_body,
428                            tool_calls: message_tool_calls,
429                            ..
430                        })) = this.messages.last_mut()
431                        {
432                            if let Some(content) = &delta.content {
433                                body.push_str(content);
434                            }
435
436                            for tool_call in delta.tool_calls {
437                                let index = tool_call.index as usize;
438                                if index >= message_tool_calls.len() {
439                                    message_tool_calls.resize_with(index + 1, Default::default);
440                                }
441                                let call = &mut message_tool_calls[index];
442
443                                if let Some(id) = &tool_call.id {
444                                    call.id.push_str(id);
445                                }
446
447                                match tool_call.variant {
448                                    Some(proto::tool_call_delta::Variant::Function(tool_call)) => {
449                                        if let Some(name) = &tool_call.name {
450                                            call.name.push_str(name);
451                                        }
452                                        if let Some(arguments) = &tool_call.arguments {
453                                            call.arguments.push_str(arguments);
454                                        }
455                                    }
456                                    None => {}
457                                }
458                            }
459
460                            *message_body =
461                                RichText::new(body.clone(), &[], &this.language_registry);
462                            cx.notify();
463                        } else {
464                            unreachable!()
465                        }
466                    })?;
467                }
468
469                anyhow::Ok(())
470            }
471            .await;
472
473            let mut tool_tasks = Vec::new();
474            this.update(cx, |this, cx| {
475                if let Some(ChatMessage::Assistant(AssistantMessage {
476                    error: message_error,
477                    tool_calls,
478                    ..
479                })) = this.messages.last_mut()
480                {
481                    if let Err(error) = complete {
482                        message_error.replace(SharedString::from(error.to_string()));
483                        cx.notify();
484                    } else {
485                        for tool_call in tool_calls.iter() {
486                            tool_tasks.push(this.tool_registry.call(tool_call, cx));
487                        }
488                    }
489                }
490            })?;
491
492            if tool_tasks.is_empty() {
493                return Ok(());
494            }
495
496            let tools = join_all(tool_tasks.into_iter()).await;
497            // If the WindowContext went away for any tool's view we don't include it
498            // especially since the below call would fail for the same reason.
499            let tools = tools.into_iter().filter_map(|tool| tool.ok()).collect();
500
501            this.update(cx, |this, cx| {
502                if let Some(ChatMessage::Assistant(AssistantMessage { tool_calls, .. })) =
503                    this.messages.last_mut()
504                {
505                    *tool_calls = tools;
506                    cx.notify();
507                }
508            })?;
509        }
510    }
511
512    fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
513        let message = ChatMessage::Assistant(AssistantMessage {
514            id: self.next_message_id.post_inc(),
515            body: RichText::default(),
516            tool_calls: Vec::new(),
517            error: None,
518        });
519        self.push_message(message, cx);
520    }
521
522    fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
523        let old_len = self.messages.len();
524        let focus_handle = Some(message.focus_handle(cx));
525        self.messages.push(message);
526        self.list_state
527            .splice_focusable(old_len..old_len, focus_handle);
528        cx.notify();
529    }
530
531    fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
532        if self.messages.is_empty() {
533            return;
534        }
535
536        self.messages.pop();
537        self.list_state
538            .splice(self.messages.len()..self.messages.len() + 1, 0);
539        cx.notify();
540    }
541
542    fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
543        if let Some(index) = self.messages.iter().position(|message| match message {
544            ChatMessage::User(message) => message.id == last_message_id,
545            ChatMessage::Assistant(message) => message.id == last_message_id,
546        }) {
547            self.list_state.splice(index + 1..self.messages.len(), 0);
548            self.messages.truncate(index + 1);
549            cx.notify();
550        }
551    }
552
553    fn is_message_collapsed(&self, id: &MessageId) -> bool {
554        self.collapsed_messages.get(id).copied().unwrap_or_default()
555    }
556
557    fn toggle_message_collapsed(&mut self, id: MessageId) {
558        let entry = self.collapsed_messages.entry(id).or_insert(false);
559        *entry = !*entry;
560    }
561
562    fn render_error(
563        &self,
564        error: Option<SharedString>,
565        _ix: usize,
566        cx: &mut ViewContext<Self>,
567    ) -> AnyElement {
568        let theme = cx.theme();
569
570        if let Some(error) = error {
571            div()
572                .py_1()
573                .px_2()
574                .neg_mx_1()
575                .rounded_md()
576                .border()
577                .border_color(theme.status().error_border)
578                // .bg(theme.status().error_background)
579                .text_color(theme.status().error)
580                .child(error.clone())
581                .into_any_element()
582        } else {
583            div().into_any_element()
584        }
585    }
586
587    fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
588        let is_last = ix == self.messages.len() - 1;
589
590        match &self.messages[ix] {
591            ChatMessage::User(UserMessage { id, body }) => div()
592                .id(SharedString::from(format!("message-{}-container", id.0)))
593                .when(!is_last, |element| element.mb_2())
594                .map(|element| {
595                    if self.editing_message_id() == Some(*id) {
596                        element.child(Composer::new(
597                            body.clone(),
598                            self.project_index_button.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.project_index_button.clone(),
776                crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
777                    .into_any_element(),
778            ))
779    }
780}
781
782#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy)]
783pub struct MessageId(usize);
784
785impl MessageId {
786    fn post_inc(&mut self) -> Self {
787        let id = *self;
788        self.0 += 1;
789        id
790    }
791}
792
793enum ChatMessage {
794    User(UserMessage),
795    Assistant(AssistantMessage),
796}
797
798impl ChatMessage {
799    fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
800        match self {
801            ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
802            ChatMessage::Assistant(_) => None,
803        }
804    }
805}
806
807struct UserMessage {
808    id: MessageId,
809    body: View<Editor>,
810}
811
812struct AssistantMessage {
813    id: MessageId,
814    body: RichText,
815    tool_calls: Vec<ToolFunctionCall>,
816    error: Option<SharedString>,
817}