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    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                        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                                            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                        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                        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}