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