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