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