assistant2.rs

   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}