assistant2.rs

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