1mod assistant_settings;
2mod attachments;
3mod completion_provider;
4mod saved_conversation;
5mod saved_conversations;
6mod tools;
7pub mod ui;
8
9use crate::saved_conversation::SavedConversationMetadata;
10use crate::ui::UserOrAssistant;
11use ::ui::{div, prelude::*, Color, Tooltip, ViewContext};
12use anyhow::{Context, Result};
13use assistant_tooling::{
14 tool_running_placeholder, AttachmentRegistry, ProjectContext, ToolFunctionCall, ToolRegistry,
15 UserAttachment,
16};
17use attachments::ActiveEditorAttachmentTool;
18use client::{proto, Client, UserStore};
19use collections::HashMap;
20use completion_provider::*;
21use editor::Editor;
22use feature_flags::FeatureFlagAppExt as _;
23use fs::Fs;
24use futures::{future::join_all, StreamExt};
25use gpui::{
26 list, AnyElement, AppContext, AsyncWindowContext, ClickEvent, EventEmitter, FocusHandle,
27 FocusableView, ListAlignment, ListState, Model, Render, Task, View, WeakView,
28};
29use language::{language_settings::SoftWrap, LanguageRegistry};
30use open_ai::{FunctionContent, ToolCall, ToolCallContent};
31use rich_text::RichText;
32use saved_conversation::{SavedAssistantMessagePart, SavedChatMessage, SavedConversation};
33use saved_conversations::SavedConversations;
34use semantic_index::{CloudEmbeddingProvider, ProjectIndex, ProjectIndexDebugView, SemanticIndex};
35use serde::{Deserialize, Serialize};
36use settings::Settings;
37use std::sync::Arc;
38use tools::{AnnotationTool, CreateBufferTool, ProjectIndexTool};
39use ui::{ActiveFileButton, Composer, ProjectIndexButton};
40use util::paths::CONVERSATIONS_DIR;
41use util::{maybe, paths::EMBEDDINGS_DIR, ResultExt};
42use workspace::{
43 dock::{DockPosition, Panel, PanelEvent},
44 Workspace,
45};
46
47pub use assistant_settings::AssistantSettings;
48
49const MAX_COMPLETION_CALLS_PER_SUBMISSION: usize = 5;
50
51#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
52pub struct Submit(SubmitMode);
53
54/// There are multiple different ways to submit a model request, represented by this enum.
55#[derive(Eq, PartialEq, Copy, Clone, Deserialize)]
56pub enum SubmitMode {
57 /// Only include the conversation.
58 Simple,
59 /// Send the current file as context.
60 CurrentFile,
61 /// Search the codebase and send relevant excerpts.
62 Codebase,
63}
64
65gpui::actions!(assistant2, [Cancel, ToggleFocus, DebugProjectIndex,]);
66gpui::impl_actions!(assistant2, [Submit]);
67
68pub fn init(client: Arc<Client>, cx: &mut AppContext) {
69 AssistantSettings::register(cx);
70
71 cx.spawn(|mut cx| {
72 let client = client.clone();
73 async move {
74 let embedding_provider = CloudEmbeddingProvider::new(client.clone());
75 let semantic_index = SemanticIndex::new(
76 EMBEDDINGS_DIR.join("semantic-index-db.0.mdb"),
77 Arc::new(embedding_provider),
78 &mut cx,
79 )
80 .await?;
81 cx.update(|cx| cx.set_global(semantic_index))
82 }
83 })
84 .detach();
85
86 cx.set_global(CompletionProvider::new(CloudCompletionProvider::new(
87 client,
88 )));
89
90 cx.observe_new_views(
91 |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
92 workspace.register_action(|workspace, _: &ToggleFocus, cx| {
93 workspace.toggle_panel_focus::<AssistantPanel>(cx);
94 });
95 workspace.register_action(|workspace, _: &DebugProjectIndex, cx| {
96 if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
97 let index = panel.read(cx).chat.read(cx).project_index.clone();
98 let view = cx.new_view(|cx| ProjectIndexDebugView::new(index, cx));
99 workspace.add_item_to_center(Box::new(view), cx);
100 }
101 });
102 },
103 )
104 .detach();
105}
106
107pub fn enabled(cx: &AppContext) -> bool {
108 cx.is_staff()
109}
110
111pub struct AssistantPanel {
112 chat: View<AssistantChat>,
113 width: Option<Pixels>,
114}
115
116impl AssistantPanel {
117 pub fn load(
118 workspace: WeakView<Workspace>,
119 cx: AsyncWindowContext,
120 ) -> Task<Result<View<Self>>> {
121 cx.spawn(|mut cx| async move {
122 let (app_state, project) = workspace.update(&mut cx, |workspace, _| {
123 (workspace.app_state().clone(), workspace.project().clone())
124 })?;
125
126 cx.new_view(|cx| {
127 let project_index = cx.update_global(|semantic_index: &mut SemanticIndex, cx| {
128 semantic_index.project_index(project.clone(), cx)
129 });
130
131 let mut tool_registry = ToolRegistry::new();
132 tool_registry
133 .register(ProjectIndexTool::new(project_index.clone()), cx)
134 .unwrap();
135 tool_registry
136 .register(
137 CreateBufferTool::new(workspace.clone(), project.clone()),
138 cx,
139 )
140 .unwrap();
141 tool_registry
142 .register(AnnotationTool::new(workspace.clone(), project.clone()), cx)
143 .unwrap();
144
145 let mut attachment_registry = AttachmentRegistry::new();
146 attachment_registry
147 .register(ActiveEditorAttachmentTool::new(workspace.clone(), cx));
148
149 Self::new(
150 project.read(cx).fs().clone(),
151 app_state.languages.clone(),
152 Arc::new(tool_registry),
153 Arc::new(attachment_registry),
154 app_state.user_store.clone(),
155 project_index,
156 workspace,
157 cx,
158 )
159 })
160 })
161 }
162
163 #[allow(clippy::too_many_arguments)]
164 pub fn new(
165 fs: Arc<dyn Fs>,
166 language_registry: Arc<LanguageRegistry>,
167 tool_registry: Arc<ToolRegistry>,
168 attachment_registry: Arc<AttachmentRegistry>,
169 user_store: Model<UserStore>,
170 project_index: Model<ProjectIndex>,
171 workspace: WeakView<Workspace>,
172 cx: &mut ViewContext<Self>,
173 ) -> Self {
174 let chat = cx.new_view(|cx| {
175 AssistantChat::new(
176 fs,
177 language_registry,
178 tool_registry.clone(),
179 attachment_registry,
180 user_store,
181 project_index,
182 workspace,
183 cx,
184 )
185 });
186
187 Self { width: None, chat }
188 }
189}
190
191impl Render for AssistantPanel {
192 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
193 div()
194 .size_full()
195 .v_flex()
196 .bg(cx.theme().colors().panel_background)
197 .child(self.chat.clone())
198 }
199}
200
201impl Panel for AssistantPanel {
202 fn persistent_name() -> &'static str {
203 "AssistantPanelv2"
204 }
205
206 fn position(&self, _cx: &WindowContext) -> workspace::dock::DockPosition {
207 // todo!("Add a setting / use assistant settings")
208 DockPosition::Right
209 }
210
211 fn position_is_valid(&self, position: workspace::dock::DockPosition) -> bool {
212 matches!(position, DockPosition::Right)
213 }
214
215 fn set_position(&mut self, _: workspace::dock::DockPosition, _: &mut ViewContext<Self>) {
216 // Do nothing until we have a setting for this
217 }
218
219 fn size(&self, _cx: &WindowContext) -> Pixels {
220 self.width.unwrap_or(px(400.))
221 }
222
223 fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
224 self.width = size;
225 cx.notify();
226 }
227
228 fn icon(&self, _cx: &WindowContext) -> Option<::ui::IconName> {
229 Some(IconName::ZedAssistant)
230 }
231
232 fn icon_tooltip(&self, _: &WindowContext) -> Option<&'static str> {
233 Some("Assistant Panel ✨")
234 }
235
236 fn toggle_action(&self) -> Box<dyn gpui::Action> {
237 Box::new(ToggleFocus)
238 }
239}
240
241impl EventEmitter<PanelEvent> for AssistantPanel {}
242
243impl FocusableView for AssistantPanel {
244 fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
245 self.chat.read(cx).composer_editor.read(cx).focus_handle(cx)
246 }
247}
248
249pub struct AssistantChat {
250 model: String,
251 messages: Vec<ChatMessage>,
252 list_state: ListState,
253 fs: Arc<dyn Fs>,
254 language_registry: Arc<LanguageRegistry>,
255 composer_editor: View<Editor>,
256 saved_conversations: View<SavedConversations>,
257 saved_conversations_open: bool,
258 project_index_button: View<ProjectIndexButton>,
259 active_file_button: Option<View<ActiveFileButton>>,
260 user_store: Model<UserStore>,
261 next_message_id: MessageId,
262 collapsed_messages: HashMap<MessageId, bool>,
263 editing_message: Option<EditingMessage>,
264 pending_completion: Option<Task<()>>,
265 tool_registry: Arc<ToolRegistry>,
266 attachment_registry: Arc<AttachmentRegistry>,
267 project_index: Model<ProjectIndex>,
268}
269
270struct EditingMessage {
271 id: MessageId,
272 old_body: Arc<str>,
273 body: View<Editor>,
274}
275
276impl AssistantChat {
277 #[allow(clippy::too_many_arguments)]
278 fn new(
279 fs: Arc<dyn Fs>,
280 language_registry: Arc<LanguageRegistry>,
281 tool_registry: Arc<ToolRegistry>,
282 attachment_registry: Arc<AttachmentRegistry>,
283 user_store: Model<UserStore>,
284 project_index: Model<ProjectIndex>,
285 workspace: WeakView<Workspace>,
286 cx: &mut ViewContext<Self>,
287 ) -> Self {
288 let model = CompletionProvider::get(cx).default_model();
289 let view = cx.view().downgrade();
290 let list_state = ListState::new(
291 0,
292 ListAlignment::Bottom,
293 px(1024.),
294 move |ix, cx: &mut WindowContext| {
295 view.update(cx, |this, cx| this.render_message(ix, cx))
296 .unwrap()
297 },
298 );
299
300 let project_index_button = cx.new_view(|cx| {
301 ProjectIndexButton::new(project_index.clone(), tool_registry.clone(), cx)
302 });
303
304 let active_file_button = match workspace.upgrade() {
305 Some(workspace) => {
306 Some(cx.new_view(
307 |cx| ActiveFileButton::new(attachment_registry.clone(), workspace, cx), //
308 ))
309 }
310 _ => None,
311 };
312
313 let saved_conversations = cx.new_view(|cx| SavedConversations::new(cx));
314 cx.spawn({
315 let fs = fs.clone();
316 let saved_conversations = saved_conversations.downgrade();
317 |_assistant_chat, mut cx| async move {
318 let saved_conversation_metadata = SavedConversationMetadata::list(fs).await?;
319
320 cx.update(|cx| {
321 saved_conversations.update(cx, |this, cx| {
322 this.init(saved_conversation_metadata, cx);
323 })
324 })??;
325
326 anyhow::Ok(())
327 }
328 })
329 .detach_and_log_err(cx);
330
331 Self {
332 model,
333 messages: Vec::new(),
334 composer_editor: cx.new_view(|cx| {
335 let mut editor = Editor::auto_height(80, cx);
336 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
337 editor.set_placeholder_text("Send a message…", cx);
338 editor
339 }),
340 saved_conversations,
341 saved_conversations_open: false,
342 list_state,
343 user_store,
344 fs,
345 language_registry,
346 project_index_button,
347 active_file_button,
348 project_index,
349 next_message_id: MessageId(0),
350 editing_message: None,
351 collapsed_messages: HashMap::default(),
352 pending_completion: None,
353 attachment_registry,
354 tool_registry,
355 }
356 }
357
358 fn editing_message_id(&self) -> Option<MessageId> {
359 self.editing_message.as_ref().map(|message| message.id)
360 }
361
362 fn focused_message_id(&self, cx: &WindowContext) -> Option<MessageId> {
363 self.messages.iter().find_map(|message| match message {
364 ChatMessage::User(message) => message
365 .body
366 .focus_handle(cx)
367 .contains_focused(cx)
368 .then_some(message.id),
369 ChatMessage::Assistant(_) => None,
370 })
371 }
372
373 fn toggle_saved_conversations(&mut self) {
374 self.saved_conversations_open = !self.saved_conversations_open;
375 }
376
377 fn cancel(&mut self, _: &Cancel, cx: &mut ViewContext<Self>) {
378 // If we're currently editing a message, cancel the edit.
379 if let Some(editing_message) = self.editing_message.take() {
380 editing_message
381 .body
382 .update(cx, |body, cx| body.set_text(editing_message.old_body, cx));
383 return;
384 }
385
386 if self.pending_completion.take().is_some() {
387 if let Some(ChatMessage::Assistant(grouping)) = self.messages.last() {
388 if grouping.messages.is_empty() {
389 self.pop_message(cx);
390 }
391 }
392 return;
393 }
394
395 cx.propagate();
396 }
397
398 fn submit(&mut self, Submit(mode): &Submit, cx: &mut ViewContext<Self>) {
399 if let Some(focused_message_id) = self.focused_message_id(cx) {
400 self.truncate_messages(focused_message_id, cx);
401 self.pending_completion.take();
402 self.composer_editor.focus_handle(cx).focus(cx);
403 if self.editing_message_id() == Some(focused_message_id) {
404 self.editing_message.take();
405 }
406 } else if self.composer_editor.focus_handle(cx).is_focused(cx) {
407 // Don't allow multiple concurrent completions.
408 if self.pending_completion.is_some() {
409 cx.propagate();
410 return;
411 }
412
413 let message = self.composer_editor.update(cx, |composer_editor, cx| {
414 let text = composer_editor.text(cx);
415 let id = self.next_message_id.post_inc();
416 let body = cx.new_view(|cx| {
417 let mut editor = Editor::auto_height(80, cx);
418 editor.set_text(text, cx);
419 editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
420 editor
421 });
422 composer_editor.clear(cx);
423
424 ChatMessage::User(UserMessage {
425 id,
426 body,
427 attachments: Vec::new(),
428 })
429 });
430 self.push_message(message, cx);
431 } else {
432 log::error!("unexpected state: no user message editor is focused.");
433 return;
434 }
435
436 let mode = *mode;
437 self.pending_completion = Some(cx.spawn(move |this, mut cx| async move {
438 let attachments_task = this.update(&mut cx, |this, cx| {
439 let attachment_registry = this.attachment_registry.clone();
440 attachment_registry.call_all_attachment_tools(cx)
441 });
442
443 let attachments = maybe!(async {
444 let attachments_task = attachments_task?;
445 let attachments = attachments_task.await?;
446
447 anyhow::Ok(attachments)
448 })
449 .await
450 .log_err()
451 .unwrap_or_default();
452
453 // Set the attachments to the _last_ user message
454 this.update(&mut cx, |this, _cx| {
455 if let Some(ChatMessage::User(message)) = this.messages.last_mut() {
456 message.attachments = attachments;
457 }
458 })
459 .log_err();
460
461 Self::request_completion(
462 this.clone(),
463 mode,
464 MAX_COMPLETION_CALLS_PER_SUBMISSION,
465 &mut cx,
466 )
467 .await
468 .log_err();
469
470 this.update(&mut cx, |this, _cx| {
471 this.pending_completion = None;
472 })
473 .context("Failed to push new user message")
474 .log_err();
475 }));
476 }
477
478 async fn request_completion(
479 this: WeakView<Self>,
480 mode: SubmitMode,
481 limit: usize,
482 cx: &mut AsyncWindowContext,
483 ) -> Result<()> {
484 let mut call_count = 0;
485 loop {
486 let complete = async {
487 let (tool_definitions, model_name, messages) = this.update(cx, |this, cx| {
488 this.push_new_assistant_message(cx);
489
490 let definitions = if call_count < limit
491 && matches!(mode, SubmitMode::Codebase | SubmitMode::Simple)
492 {
493 this.tool_registry.definitions()
494 } else {
495 Vec::new()
496 };
497 call_count += 1;
498
499 (
500 definitions,
501 this.model.clone(),
502 this.completion_messages(cx),
503 )
504 })?;
505
506 let messages = messages.await?;
507
508 let completion = cx.update(|cx| {
509 CompletionProvider::get(cx).complete(
510 model_name,
511 messages,
512 Vec::new(),
513 1.0,
514 tool_definitions,
515 )
516 });
517
518 let mut stream = completion?.await?;
519 let mut body = String::new();
520 while let Some(delta) = stream.next().await {
521 let delta = delta?;
522 this.update(cx, |this, cx| {
523 if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
524 this.messages.last_mut()
525 {
526 if messages.is_empty() {
527 messages.push(AssistantMessagePart {
528 body: RichText::default(),
529 tool_calls: Vec::new(),
530 })
531 }
532
533 let message = messages.last_mut().unwrap();
534
535 if let Some(content) = &delta.content {
536 body.push_str(content);
537 }
538
539 for tool_call_delta in delta.tool_calls {
540 let index = tool_call_delta.index as usize;
541 if index >= message.tool_calls.len() {
542 message.tool_calls.resize_with(index + 1, Default::default);
543 }
544 let tool_call = &mut message.tool_calls[index];
545
546 if let Some(id) = &tool_call_delta.id {
547 tool_call.id.push_str(id);
548 }
549
550 match tool_call_delta.variant {
551 Some(proto::tool_call_delta::Variant::Function(
552 tool_call_delta,
553 )) => {
554 this.tool_registry.update_tool_call(
555 tool_call,
556 tool_call_delta.name.as_deref(),
557 tool_call_delta.arguments.as_deref(),
558 cx,
559 );
560 }
561 None => {}
562 }
563 }
564
565 message.body =
566 RichText::new(body.clone(), &[], &this.language_registry);
567 cx.notify();
568 } else {
569 unreachable!()
570 }
571 })?;
572 }
573
574 anyhow::Ok(())
575 }
576 .await;
577
578 let mut tool_tasks = Vec::new();
579 this.update(cx, |this, cx| {
580 if let Some(ChatMessage::Assistant(AssistantMessage {
581 error: message_error,
582 messages,
583 ..
584 })) = this.messages.last_mut()
585 {
586 if let Err(error) = complete {
587 message_error.replace(SharedString::from(error.to_string()));
588 cx.notify();
589 } else {
590 if let Some(current_message) = messages.last_mut() {
591 for tool_call in current_message.tool_calls.iter() {
592 tool_tasks
593 .extend(this.tool_registry.execute_tool_call(&tool_call, cx));
594 }
595 }
596 }
597 }
598 })?;
599
600 // This ends recursion on calling for responses after tools
601 if tool_tasks.is_empty() {
602 return Ok(());
603 }
604
605 join_all(tool_tasks.into_iter()).await;
606 }
607 }
608
609 fn push_new_assistant_message(&mut self, cx: &mut ViewContext<Self>) {
610 // If the last message is a grouped assistant message, add to the grouped message
611 if let Some(ChatMessage::Assistant(AssistantMessage { messages, .. })) =
612 self.messages.last_mut()
613 {
614 messages.push(AssistantMessagePart {
615 body: RichText::default(),
616 tool_calls: Vec::new(),
617 });
618 return;
619 }
620
621 let message = ChatMessage::Assistant(AssistantMessage {
622 id: self.next_message_id.post_inc(),
623 messages: vec![AssistantMessagePart {
624 body: RichText::default(),
625 tool_calls: Vec::new(),
626 }],
627 error: None,
628 });
629 self.push_message(message, cx);
630 }
631
632 fn push_message(&mut self, message: ChatMessage, cx: &mut ViewContext<Self>) {
633 let old_len = self.messages.len();
634 let focus_handle = Some(message.focus_handle(cx));
635 self.messages.push(message);
636 self.list_state
637 .splice_focusable(old_len..old_len, focus_handle);
638 cx.notify();
639 }
640
641 fn pop_message(&mut self, cx: &mut ViewContext<Self>) {
642 if self.messages.is_empty() {
643 return;
644 }
645
646 self.messages.pop();
647 self.list_state
648 .splice(self.messages.len()..self.messages.len() + 1, 0);
649 cx.notify();
650 }
651
652 fn truncate_messages(&mut self, last_message_id: MessageId, cx: &mut ViewContext<Self>) {
653 if let Some(index) = self.messages.iter().position(|message| match message {
654 ChatMessage::User(message) => message.id == last_message_id,
655 ChatMessage::Assistant(message) => message.id == last_message_id,
656 }) {
657 self.list_state.splice(index + 1..self.messages.len(), 0);
658 self.messages.truncate(index + 1);
659 cx.notify();
660 }
661 }
662
663 fn is_message_collapsed(&self, id: &MessageId) -> bool {
664 self.collapsed_messages.get(id).copied().unwrap_or_default()
665 }
666
667 fn toggle_message_collapsed(&mut self, id: MessageId) {
668 let entry = self.collapsed_messages.entry(id).or_insert(false);
669 *entry = !*entry;
670 }
671
672 fn reset(&mut self) {
673 self.messages.clear();
674 self.list_state.reset(0);
675 self.editing_message.take();
676 self.collapsed_messages.clear();
677 }
678
679 fn new_conversation(&mut self, cx: &mut ViewContext<Self>) {
680 let messages = std::mem::take(&mut self.messages)
681 .into_iter()
682 .map(|message| self.serialize_message(message, cx))
683 .collect::<Vec<_>>();
684
685 self.reset();
686
687 let title = messages
688 .first()
689 .map(|message| match message {
690 SavedChatMessage::User { body, .. } => body.clone(),
691 SavedChatMessage::Assistant { messages, .. } => messages
692 .first()
693 .map(|message| message.body.to_string())
694 .unwrap_or_default(),
695 })
696 .unwrap_or_else(|| "A conversation with the assistant.".to_string());
697
698 let saved_conversation = SavedConversation {
699 version: "0.3.0".to_string(),
700 title,
701 messages,
702 };
703
704 let discriminant = 1;
705
706 let path = CONVERSATIONS_DIR.join(&format!(
707 "{title} - {discriminant}.zed.{version}.json",
708 title = saved_conversation.title,
709 version = saved_conversation.version
710 ));
711
712 cx.spawn({
713 let fs = self.fs.clone();
714 |_this, _cx| async move {
715 fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
716 fs.atomic_write(path, serde_json::to_string(&saved_conversation)?)
717 .await?;
718
719 anyhow::Ok(())
720 }
721 })
722 .detach_and_log_err(cx);
723 }
724
725 fn render_error(
726 &self,
727 error: Option<SharedString>,
728 _ix: usize,
729 cx: &mut ViewContext<Self>,
730 ) -> AnyElement {
731 let theme = cx.theme();
732
733 if let Some(error) = error {
734 div()
735 .py_1()
736 .px_2()
737 .mx_neg_1()
738 .rounded_md()
739 .border_1()
740 .border_color(theme.status().error_border)
741 // .bg(theme.status().error_background)
742 .text_color(theme.status().error)
743 .child(error.clone())
744 .into_any_element()
745 } else {
746 div().into_any_element()
747 }
748 }
749
750 fn render_message(&self, ix: usize, cx: &mut ViewContext<Self>) -> AnyElement {
751 let is_first = ix == 0;
752 let is_last = ix == self.messages.len().saturating_sub(1);
753
754 let padding = Spacing::Large.rems(cx);
755
756 // Whenever there's a run of assistant messages, group as one Assistant UI element
757
758 match &self.messages[ix] {
759 ChatMessage::User(UserMessage {
760 id,
761 body,
762 attachments,
763 }) => div()
764 .id(SharedString::from(format!("message-{}-container", id.0)))
765 .when(is_first, |this| this.pt(padding))
766 .map(|element| {
767 if self.editing_message_id() == Some(*id) {
768 element.child(Composer::new(
769 body.clone(),
770 self.project_index_button.clone(),
771 self.active_file_button.clone(),
772 crate::ui::ModelSelector::new(
773 cx.view().downgrade(),
774 self.model.clone(),
775 )
776 .into_any_element(),
777 ))
778 } else {
779 element
780 .on_click(cx.listener({
781 let id = *id;
782 let body = body.clone();
783 move |assistant_chat, event: &ClickEvent, cx| {
784 if event.up.click_count == 2 {
785 assistant_chat.editing_message = Some(EditingMessage {
786 id,
787 body: body.clone(),
788 old_body: body.read(cx).text(cx).into(),
789 });
790 body.focus_handle(cx).focus(cx);
791 }
792 }
793 }))
794 .child(
795 crate::ui::ChatMessage::new(
796 *id,
797 UserOrAssistant::User(self.user_store.read(cx).current_user()),
798 // todo!(): clean up the vec usage
799 vec![
800 RichText::new(
801 body.read(cx).text(cx),
802 &[],
803 &self.language_registry,
804 )
805 .element(ElementId::from(id.0), cx),
806 h_flex()
807 .gap_2()
808 .children(
809 attachments
810 .iter()
811 .map(|attachment| attachment.view.clone()),
812 )
813 .into_any_element(),
814 ],
815 self.is_message_collapsed(id),
816 Box::new(cx.listener({
817 let id = *id;
818 move |assistant_chat, _event, _cx| {
819 assistant_chat.toggle_message_collapsed(id)
820 }
821 })),
822 )
823 // TODO: Wire up selections.
824 .selected(is_last),
825 )
826 }
827 })
828 .into_any(),
829 ChatMessage::Assistant(AssistantMessage {
830 id,
831 messages,
832 error,
833 ..
834 }) => {
835 let mut message_elements = Vec::new();
836
837 for message in messages {
838 if !message.body.text.is_empty() {
839 message_elements.push(
840 div()
841 // todo!(): The element Id will need to be a combo of the base ID and the index within the grouping
842 .child(message.body.element(ElementId::from(id.0), cx))
843 .into_any_element(),
844 )
845 }
846
847 let tools = message
848 .tool_calls
849 .iter()
850 .map(|tool_call| self.tool_registry.render_tool_call(tool_call, cx))
851 .collect::<Vec<AnyElement>>();
852
853 if !tools.is_empty() {
854 message_elements.push(div().children(tools).into_any_element())
855 }
856 }
857
858 if message_elements.is_empty() {
859 message_elements.push(tool_running_placeholder());
860 }
861
862 div()
863 .when(is_first, |this| this.pt(padding))
864 .child(
865 crate::ui::ChatMessage::new(
866 *id,
867 UserOrAssistant::Assistant,
868 message_elements,
869 self.is_message_collapsed(id),
870 Box::new(cx.listener({
871 let id = *id;
872 move |assistant_chat, _event, _cx| {
873 assistant_chat.toggle_message_collapsed(id)
874 }
875 })),
876 )
877 // TODO: Wire up selections.
878 .selected(is_last),
879 )
880 .child(self.render_error(error.clone(), ix, cx))
881 .into_any()
882 }
883 }
884 }
885
886 fn completion_messages(&self, cx: &mut WindowContext) -> Task<Result<Vec<CompletionMessage>>> {
887 let project_index = self.project_index.read(cx);
888 let project = project_index.project();
889 let fs = project_index.fs();
890
891 let mut project_context = ProjectContext::new(project, fs);
892 let mut completion_messages = Vec::new();
893
894 for message in &self.messages {
895 match message {
896 ChatMessage::User(UserMessage {
897 body, attachments, ..
898 }) => {
899 for attachment in attachments {
900 if let Some(content) = attachment.generate(&mut project_context, cx) {
901 completion_messages.push(CompletionMessage::System { content });
902 }
903 }
904
905 // Show user's message last so that the assistant is grounded in the user's request
906 completion_messages.push(CompletionMessage::User {
907 content: body.read(cx).text(cx),
908 });
909 }
910 ChatMessage::Assistant(AssistantMessage { messages, .. }) => {
911 for message in messages {
912 let body = message.body.clone();
913
914 if body.text.is_empty() && message.tool_calls.is_empty() {
915 continue;
916 }
917
918 let tool_calls_from_assistant = message
919 .tool_calls
920 .iter()
921 .map(|tool_call| ToolCall {
922 content: ToolCallContent::Function {
923 function: FunctionContent {
924 name: tool_call.name.clone(),
925 arguments: tool_call.arguments.clone(),
926 },
927 },
928 id: tool_call.id.clone(),
929 })
930 .collect();
931
932 completion_messages.push(CompletionMessage::Assistant {
933 content: Some(body.text.to_string()),
934 tool_calls: tool_calls_from_assistant,
935 });
936
937 for tool_call in &message.tool_calls {
938 // Every tool call _must_ have a result by ID, otherwise OpenAI will error.
939 let content = self.tool_registry.content_for_tool_call(
940 tool_call,
941 &mut project_context,
942 cx,
943 );
944 completion_messages.push(CompletionMessage::Tool {
945 content,
946 tool_call_id: tool_call.id.clone(),
947 });
948 }
949 }
950 }
951 }
952 }
953
954 let system_message = project_context.generate_system_message(cx);
955
956 cx.background_executor().spawn(async move {
957 let content = system_message.await?;
958 completion_messages.insert(0, CompletionMessage::System { content });
959 Ok(completion_messages)
960 })
961 }
962
963 fn serialize_message(
964 &self,
965 message: ChatMessage,
966 cx: &mut ViewContext<AssistantChat>,
967 ) -> SavedChatMessage {
968 match message {
969 ChatMessage::User(message) => SavedChatMessage::User {
970 id: message.id,
971 body: message.body.read(cx).text(cx),
972 attachments: message
973 .attachments
974 .iter()
975 .map(|attachment| {
976 self.attachment_registry
977 .serialize_user_attachment(attachment)
978 })
979 .collect(),
980 },
981 ChatMessage::Assistant(message) => SavedChatMessage::Assistant {
982 id: message.id,
983 error: message.error,
984 messages: message
985 .messages
986 .iter()
987 .map(|message| SavedAssistantMessagePart {
988 body: message.body.text.clone(),
989 tool_calls: message
990 .tool_calls
991 .iter()
992 .filter_map(|tool_call| {
993 self.tool_registry
994 .serialize_tool_call(tool_call, cx)
995 .log_err()
996 })
997 .collect(),
998 })
999 .collect(),
1000 },
1001 }
1002 }
1003}
1004
1005impl Render for AssistantChat {
1006 fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1007 let header_height = Spacing::Small.rems(cx) * 2.0 + ButtonSize::Default.rems();
1008
1009 div()
1010 .relative()
1011 .flex_1()
1012 .v_flex()
1013 .key_context("AssistantChat")
1014 .on_action(cx.listener(Self::submit))
1015 .on_action(cx.listener(Self::cancel))
1016 .text_color(Color::Default.color(cx))
1017 .child(list(self.list_state.clone()).flex_1().pt(header_height))
1018 .child(
1019 h_flex()
1020 .absolute()
1021 .top_0()
1022 .justify_between()
1023 .w_full()
1024 .h(header_height)
1025 .p(Spacing::Small.rems(cx))
1026 .child(
1027 IconButton::new(
1028 "toggle-saved-conversations",
1029 if self.saved_conversations_open {
1030 IconName::ChevronRight
1031 } else {
1032 IconName::ChevronLeft
1033 },
1034 )
1035 .on_click(cx.listener(|this, _event, _cx| {
1036 this.toggle_saved_conversations();
1037 }))
1038 .tooltip(move |cx| Tooltip::text("Switch Conversations", cx)),
1039 )
1040 .child(
1041 h_flex()
1042 .gap(Spacing::Large.rems(cx))
1043 .child(
1044 IconButton::new("new-conversation", IconName::Plus)
1045 .on_click(cx.listener(move |this, _event, cx| {
1046 this.new_conversation(cx);
1047 }))
1048 .tooltip(move |cx| Tooltip::text("New Conversation", cx)),
1049 )
1050 .child(
1051 IconButton::new("assistant-menu", IconName::Menu)
1052 .disabled(true)
1053 .tooltip(move |cx| {
1054 Tooltip::text(
1055 "Coming soon – Assistant settings & controls",
1056 cx,
1057 )
1058 }),
1059 ),
1060 ),
1061 )
1062 .when(self.saved_conversations_open, |element| {
1063 element.child(
1064 h_flex()
1065 .absolute()
1066 .top(header_height)
1067 .w_full()
1068 .child(self.saved_conversations.clone()),
1069 )
1070 })
1071 .child(Composer::new(
1072 self.composer_editor.clone(),
1073 self.project_index_button.clone(),
1074 self.active_file_button.clone(),
1075 crate::ui::ModelSelector::new(cx.view().downgrade(), self.model.clone())
1076 .into_any_element(),
1077 ))
1078 }
1079}
1080
1081#[derive(Debug, PartialEq, Eq, PartialOrd, Ord, Hash, Clone, Copy, Serialize, Deserialize)]
1082pub struct MessageId(usize);
1083
1084impl MessageId {
1085 fn post_inc(&mut self) -> Self {
1086 let id = *self;
1087 self.0 += 1;
1088 id
1089 }
1090}
1091
1092enum ChatMessage {
1093 User(UserMessage),
1094 Assistant(AssistantMessage),
1095}
1096
1097impl ChatMessage {
1098 fn focus_handle(&self, cx: &AppContext) -> Option<FocusHandle> {
1099 match self {
1100 ChatMessage::User(UserMessage { body, .. }) => Some(body.focus_handle(cx)),
1101 ChatMessage::Assistant(_) => None,
1102 }
1103 }
1104}
1105
1106struct UserMessage {
1107 pub id: MessageId,
1108 pub body: View<Editor>,
1109 pub attachments: Vec<UserAttachment>,
1110}
1111
1112struct AssistantMessagePart {
1113 pub body: RichText,
1114 pub tool_calls: Vec<ToolFunctionCall>,
1115}
1116
1117struct AssistantMessage {
1118 pub id: MessageId,
1119 pub messages: Vec<AssistantMessagePart>,
1120 pub error: Option<SharedString>,
1121}