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}