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