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