assistant_panel.rs

   1use crate::{
   2    assistant_settings::{AssistantDockPosition, AssistantSettings, OpenAiModel},
   3    codegen::{self, Codegen, CodegenKind},
   4    prompts::generate_content_prompt,
   5    Assist, CycleMessageRole, InlineAssist, MessageId, MessageMetadata, MessageStatus,
   6    NewConversation, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
   7    SavedMessage, Split, ToggleFocus, ToggleIncludeConversation, ToggleRetrieveContext,
   8};
   9use ai::prompts::repository_context::PromptCodeSnippet;
  10use ai::{
  11    auth::ProviderCredential,
  12    completion::{CompletionProvider, CompletionRequest},
  13    providers::open_ai::{OpenAiCompletionProvider, OpenAiRequest, RequestMessage},
  14};
  15use anyhow::{anyhow, Result};
  16use chrono::{DateTime, Local};
  17use client::telemetry::AssistantKind;
  18use collections::{hash_map, HashMap, HashSet, VecDeque};
  19use editor::{
  20    actions::{MoveDown, MoveUp},
  21    display_map::{
  22        BlockContext, BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint,
  23    },
  24    scroll::{Autoscroll, AutoscrollStrategy},
  25    Anchor, Editor, EditorElement, EditorEvent, EditorStyle, MultiBufferSnapshot, ToOffset,
  26    ToPoint,
  27};
  28use fs::Fs;
  29use futures::StreamExt;
  30use gpui::{
  31    canvas, div, point, relative, rems, uniform_list, Action, AnyElement, AppContext,
  32    AsyncAppContext, AsyncWindowContext, AvailableSpace, ClipboardItem, Context, EventEmitter,
  33    FocusHandle, FocusableView, FontStyle, FontWeight, HighlightStyle, InteractiveElement,
  34    IntoElement, Model, ModelContext, ParentElement, Pixels, PromptLevel, Render, SharedString,
  35    StatefulInteractiveElement, Styled, Subscription, Task, TextStyle, UniformListScrollHandle,
  36    View, ViewContext, VisualContext, WeakModel, WeakView, WhiteSpace, WindowContext,
  37};
  38use language::{language_settings::SoftWrap, Buffer, BufferId, LanguageRegistry, ToOffset as _};
  39use project::Project;
  40use search::{buffer_search::DivRegistrar, BufferSearchBar};
  41use semantic_index::{SemanticIndex, SemanticIndexStatus};
  42use settings::Settings;
  43use std::{
  44    cell::Cell,
  45    cmp,
  46    fmt::Write,
  47    iter,
  48    ops::Range,
  49    path::{Path, PathBuf},
  50    rc::Rc,
  51    sync::Arc,
  52    time::{Duration, Instant},
  53};
  54use theme::ThemeSettings;
  55use ui::{
  56    prelude::*,
  57    utils::{DateTimeType, FormatDistance},
  58    ButtonLike, Tab, TabBar, Tooltip,
  59};
  60use util::{paths::CONVERSATIONS_DIR, post_inc, ResultExt, TryFutureExt};
  61use uuid::Uuid;
  62use workspace::{
  63    dock::{DockPosition, Panel, PanelEvent},
  64    searchable::Direction,
  65    Save, Toast, ToggleZoom, Toolbar, Workspace,
  66};
  67
  68pub fn init(cx: &mut AppContext) {
  69    AssistantSettings::register(cx);
  70    cx.observe_new_views(
  71        |workspace: &mut Workspace, _cx: &mut ViewContext<Workspace>| {
  72            workspace
  73                .register_action(|workspace, _: &ToggleFocus, cx| {
  74                    workspace.toggle_panel_focus::<AssistantPanel>(cx);
  75                })
  76                .register_action(AssistantPanel::inline_assist)
  77                .register_action(AssistantPanel::cancel_last_inline_assist)
  78                .register_action(ConversationEditor::quote_selection);
  79        },
  80    )
  81    .detach();
  82}
  83
  84pub struct AssistantPanel {
  85    workspace: WeakView<Workspace>,
  86    width: Option<Pixels>,
  87    height: Option<Pixels>,
  88    active_editor_index: Option<usize>,
  89    prev_active_editor_index: Option<usize>,
  90    editors: Vec<View<ConversationEditor>>,
  91    saved_conversations: Vec<SavedConversationMetadata>,
  92    saved_conversations_scroll_handle: UniformListScrollHandle,
  93    zoomed: bool,
  94    focus_handle: FocusHandle,
  95    toolbar: View<Toolbar>,
  96    completion_provider: Arc<dyn CompletionProvider>,
  97    api_key_editor: Option<View<Editor>>,
  98    languages: Arc<LanguageRegistry>,
  99    fs: Arc<dyn Fs>,
 100    subscriptions: Vec<Subscription>,
 101    next_inline_assist_id: usize,
 102    pending_inline_assists: HashMap<usize, PendingInlineAssist>,
 103    pending_inline_assist_ids_by_editor: HashMap<WeakView<Editor>, Vec<usize>>,
 104    include_conversation_in_next_inline_assist: bool,
 105    inline_prompt_history: VecDeque<String>,
 106    _watch_saved_conversations: Task<Result<()>>,
 107    semantic_index: Option<Model<SemanticIndex>>,
 108    retrieve_context_in_next_inline_assist: bool,
 109}
 110
 111impl AssistantPanel {
 112    const INLINE_PROMPT_HISTORY_MAX_LEN: usize = 20;
 113
 114    pub fn load(
 115        workspace: WeakView<Workspace>,
 116        cx: AsyncWindowContext,
 117    ) -> Task<Result<View<Self>>> {
 118        cx.spawn(|mut cx| async move {
 119            let fs = workspace.update(&mut cx, |workspace, _| workspace.app_state().fs.clone())?;
 120            let saved_conversations = SavedConversationMetadata::list(fs.clone())
 121                .await
 122                .log_err()
 123                .unwrap_or_default();
 124            // Defaulting currently to GPT4, allow for this to be set via config.
 125            let completion_provider =
 126                OpenAiCompletionProvider::new("gpt-4".into(), cx.background_executor().clone())
 127                    .await;
 128
 129            // TODO: deserialize state.
 130            let workspace_handle = workspace.clone();
 131            workspace.update(&mut cx, |workspace, cx| {
 132                cx.new_view::<Self>(|cx| {
 133                    const CONVERSATION_WATCH_DURATION: Duration = Duration::from_millis(100);
 134                    let _watch_saved_conversations = cx.spawn(move |this, mut cx| async move {
 135                        let mut events = fs
 136                            .watch(&CONVERSATIONS_DIR, CONVERSATION_WATCH_DURATION)
 137                            .await;
 138                        while events.next().await.is_some() {
 139                            let saved_conversations = SavedConversationMetadata::list(fs.clone())
 140                                .await
 141                                .log_err()
 142                                .unwrap_or_default();
 143                            this.update(&mut cx, |this, cx| {
 144                                this.saved_conversations = saved_conversations;
 145                                cx.notify();
 146                            })
 147                            .ok();
 148                        }
 149
 150                        anyhow::Ok(())
 151                    });
 152
 153                    let toolbar = cx.new_view(|cx| {
 154                        let mut toolbar = Toolbar::new();
 155                        toolbar.set_can_navigate(false, cx);
 156                        toolbar.add_item(cx.new_view(|cx| BufferSearchBar::new(cx)), cx);
 157                        toolbar
 158                    });
 159
 160                    let semantic_index = SemanticIndex::global(cx);
 161
 162                    let focus_handle = cx.focus_handle();
 163                    cx.on_focus_in(&focus_handle, Self::focus_in).detach();
 164                    cx.on_focus_out(&focus_handle, Self::focus_out).detach();
 165
 166                    Self {
 167                        workspace: workspace_handle,
 168                        active_editor_index: Default::default(),
 169                        prev_active_editor_index: Default::default(),
 170                        editors: Default::default(),
 171                        saved_conversations,
 172                        saved_conversations_scroll_handle: Default::default(),
 173                        zoomed: false,
 174                        focus_handle,
 175                        toolbar,
 176                        completion_provider: Arc::new(completion_provider),
 177                        api_key_editor: None,
 178                        languages: workspace.app_state().languages.clone(),
 179                        fs: workspace.app_state().fs.clone(),
 180                        width: None,
 181                        height: None,
 182                        subscriptions: Default::default(),
 183                        next_inline_assist_id: 0,
 184                        pending_inline_assists: Default::default(),
 185                        pending_inline_assist_ids_by_editor: Default::default(),
 186                        include_conversation_in_next_inline_assist: false,
 187                        inline_prompt_history: Default::default(),
 188                        _watch_saved_conversations,
 189                        semantic_index,
 190                        retrieve_context_in_next_inline_assist: false,
 191                    }
 192                })
 193            })
 194        })
 195    }
 196
 197    fn focus_in(&mut self, cx: &mut ViewContext<Self>) {
 198        self.toolbar
 199            .update(cx, |toolbar, cx| toolbar.focus_changed(true, cx));
 200        cx.notify();
 201        if self.focus_handle.is_focused(cx) {
 202            if self.has_credentials() {
 203                if let Some(editor) = self.active_editor() {
 204                    cx.focus_view(editor);
 205                }
 206            }
 207
 208            if let Some(api_key_editor) = self.api_key_editor.as_ref() {
 209                cx.focus_view(api_key_editor);
 210            }
 211        }
 212    }
 213
 214    fn focus_out(&mut self, cx: &mut ViewContext<Self>) {
 215        self.toolbar
 216            .update(cx, |toolbar, cx| toolbar.focus_changed(false, cx));
 217        cx.notify();
 218    }
 219
 220    pub fn inline_assist(
 221        workspace: &mut Workspace,
 222        _: &InlineAssist,
 223        cx: &mut ViewContext<Workspace>,
 224    ) {
 225        let Some(assistant) = workspace.panel::<AssistantPanel>(cx) else {
 226            return;
 227        };
 228        let active_editor = if let Some(active_editor) = workspace
 229            .active_item(cx)
 230            .and_then(|item| item.act_as::<Editor>(cx))
 231        {
 232            active_editor
 233        } else {
 234            return;
 235        };
 236        let project = workspace.project().clone();
 237
 238        if assistant.update(cx, |assistant, _| assistant.has_credentials()) {
 239            assistant.update(cx, |assistant, cx| {
 240                assistant.new_inline_assist(&active_editor, cx, &project)
 241            });
 242        } else {
 243            let assistant = assistant.downgrade();
 244            cx.spawn(|workspace, mut cx| async move {
 245                assistant
 246                    .update(&mut cx, |assistant, cx| assistant.load_credentials(cx))?
 247                    .await;
 248                if assistant.update(&mut cx, |assistant, _| assistant.has_credentials())? {
 249                    assistant.update(&mut cx, |assistant, cx| {
 250                        assistant.new_inline_assist(&active_editor, cx, &project)
 251                    })?;
 252                } else {
 253                    workspace.update(&mut cx, |workspace, cx| {
 254                        workspace.focus_panel::<AssistantPanel>(cx)
 255                    })?;
 256                }
 257
 258                anyhow::Ok(())
 259            })
 260            .detach_and_log_err(cx)
 261        }
 262    }
 263
 264    fn new_inline_assist(
 265        &mut self,
 266        editor: &View<Editor>,
 267        cx: &mut ViewContext<Self>,
 268        project: &Model<Project>,
 269    ) {
 270        let selection = editor.read(cx).selections.newest_anchor().clone();
 271        if selection.start.excerpt_id != selection.end.excerpt_id {
 272            return;
 273        }
 274        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
 275
 276        // Extend the selection to the start and the end of the line.
 277        let mut point_selection = selection.map(|selection| selection.to_point(&snapshot));
 278        if point_selection.end > point_selection.start {
 279            point_selection.start.column = 0;
 280            // If the selection ends at the start of the line, we don't want to include it.
 281            if point_selection.end.column == 0 {
 282                point_selection.end.row -= 1;
 283            }
 284            point_selection.end.column = snapshot.line_len(point_selection.end.row);
 285        }
 286
 287        let codegen_kind = if point_selection.start == point_selection.end {
 288            CodegenKind::Generate {
 289                position: snapshot.anchor_after(point_selection.start),
 290            }
 291        } else {
 292            CodegenKind::Transform {
 293                range: snapshot.anchor_before(point_selection.start)
 294                    ..snapshot.anchor_after(point_selection.end),
 295            }
 296        };
 297
 298        let inline_assist_id = post_inc(&mut self.next_inline_assist_id);
 299        let provider = self.completion_provider.clone();
 300
 301        let codegen = cx.new_model(|cx| {
 302            Codegen::new(editor.read(cx).buffer().clone(), codegen_kind, provider, cx)
 303        });
 304
 305        if let Some(semantic_index) = self.semantic_index.clone() {
 306            let project = project.clone();
 307            cx.spawn(|_, mut cx| async move {
 308                let previously_indexed = semantic_index
 309                    .update(&mut cx, |index, cx| {
 310                        index.project_previously_indexed(&project, cx)
 311                    })?
 312                    .await
 313                    .unwrap_or(false);
 314                if previously_indexed {
 315                    let _ = semantic_index
 316                        .update(&mut cx, |index, cx| {
 317                            index.index_project(project.clone(), cx)
 318                        })?
 319                        .await;
 320                }
 321                anyhow::Ok(())
 322            })
 323            .detach_and_log_err(cx);
 324        }
 325
 326        let measurements = Rc::new(Cell::new(BlockMeasurements::default()));
 327        let inline_assistant = cx.new_view(|cx| {
 328            InlineAssistant::new(
 329                inline_assist_id,
 330                measurements.clone(),
 331                self.include_conversation_in_next_inline_assist,
 332                self.inline_prompt_history.clone(),
 333                codegen.clone(),
 334                self.workspace.clone(),
 335                cx,
 336                self.retrieve_context_in_next_inline_assist,
 337                self.semantic_index.clone(),
 338                project.clone(),
 339            )
 340        });
 341        let block_id = editor.update(cx, |editor, cx| {
 342            editor.change_selections(None, cx, |selections| {
 343                selections.select_anchor_ranges([selection.head()..selection.head()])
 344            });
 345            editor.insert_blocks(
 346                [BlockProperties {
 347                    style: BlockStyle::Flex,
 348                    position: snapshot.anchor_before(point_selection.head()),
 349                    height: 2,
 350                    render: Arc::new({
 351                        let inline_assistant = inline_assistant.clone();
 352                        move |cx: &mut BlockContext| {
 353                            measurements.set(BlockMeasurements {
 354                                anchor_x: cx.anchor_x,
 355                                gutter_width: cx.gutter_width,
 356                            });
 357                            inline_assistant.clone().into_any_element()
 358                        }
 359                    }),
 360                    disposition: if selection.reversed {
 361                        BlockDisposition::Above
 362                    } else {
 363                        BlockDisposition::Below
 364                    },
 365                }],
 366                Some(Autoscroll::Strategy(AutoscrollStrategy::Newest)),
 367                cx,
 368            )[0]
 369        });
 370
 371        self.pending_inline_assists.insert(
 372            inline_assist_id,
 373            PendingInlineAssist {
 374                editor: editor.downgrade(),
 375                inline_assistant: Some((block_id, inline_assistant.clone())),
 376                codegen: codegen.clone(),
 377                project: project.downgrade(),
 378                _subscriptions: vec![
 379                    cx.subscribe(&inline_assistant, Self::handle_inline_assistant_event),
 380                    cx.subscribe(editor, {
 381                        let inline_assistant = inline_assistant.downgrade();
 382                        move |_, editor, event, cx| {
 383                            if let Some(inline_assistant) = inline_assistant.upgrade() {
 384                                if let EditorEvent::SelectionsChanged { local } = event {
 385                                    if *local
 386                                        && inline_assistant.focus_handle(cx).contains_focused(cx)
 387                                    {
 388                                        cx.focus_view(&editor);
 389                                    }
 390                                }
 391                            }
 392                        }
 393                    }),
 394                    cx.observe(&codegen, {
 395                        let editor = editor.downgrade();
 396                        move |this, _, cx| {
 397                            if let Some(editor) = editor.upgrade() {
 398                                this.update_highlights_for_editor(&editor, cx);
 399                            }
 400                        }
 401                    }),
 402                    cx.subscribe(&codegen, move |this, codegen, event, cx| match event {
 403                        codegen::Event::Undone => {
 404                            this.finish_inline_assist(inline_assist_id, false, cx)
 405                        }
 406                        codegen::Event::Finished => {
 407                            let pending_assist = if let Some(pending_assist) =
 408                                this.pending_inline_assists.get(&inline_assist_id)
 409                            {
 410                                pending_assist
 411                            } else {
 412                                return;
 413                            };
 414
 415                            let error = codegen
 416                                .read(cx)
 417                                .error()
 418                                .map(|error| format!("Inline assistant error: {}", error));
 419                            if let Some(error) = error {
 420                                if pending_assist.inline_assistant.is_none() {
 421                                    if let Some(workspace) = this.workspace.upgrade() {
 422                                        workspace.update(cx, |workspace, cx| {
 423                                            workspace.show_toast(
 424                                                Toast::new(inline_assist_id, error),
 425                                                cx,
 426                                            );
 427                                        })
 428                                    }
 429
 430                                    this.finish_inline_assist(inline_assist_id, false, cx);
 431                                }
 432                            } else {
 433                                this.finish_inline_assist(inline_assist_id, false, cx);
 434                            }
 435                        }
 436                    }),
 437                ],
 438            },
 439        );
 440        self.pending_inline_assist_ids_by_editor
 441            .entry(editor.downgrade())
 442            .or_default()
 443            .push(inline_assist_id);
 444        self.update_highlights_for_editor(&editor, cx);
 445    }
 446
 447    fn handle_inline_assistant_event(
 448        &mut self,
 449        inline_assistant: View<InlineAssistant>,
 450        event: &InlineAssistantEvent,
 451        cx: &mut ViewContext<Self>,
 452    ) {
 453        let assist_id = inline_assistant.read(cx).id;
 454        match event {
 455            InlineAssistantEvent::Confirmed {
 456                prompt,
 457                include_conversation,
 458                retrieve_context,
 459            } => {
 460                self.confirm_inline_assist(
 461                    assist_id,
 462                    prompt,
 463                    *include_conversation,
 464                    cx,
 465                    *retrieve_context,
 466                );
 467            }
 468            InlineAssistantEvent::Canceled => {
 469                self.finish_inline_assist(assist_id, true, cx);
 470            }
 471            InlineAssistantEvent::Dismissed => {
 472                self.hide_inline_assist(assist_id, cx);
 473            }
 474            InlineAssistantEvent::IncludeConversationToggled {
 475                include_conversation,
 476            } => {
 477                self.include_conversation_in_next_inline_assist = *include_conversation;
 478            }
 479            InlineAssistantEvent::RetrieveContextToggled { retrieve_context } => {
 480                self.retrieve_context_in_next_inline_assist = *retrieve_context
 481            }
 482        }
 483    }
 484
 485    fn cancel_last_inline_assist(
 486        workspace: &mut Workspace,
 487        _: &editor::actions::Cancel,
 488        cx: &mut ViewContext<Workspace>,
 489    ) {
 490        if let Some(panel) = workspace.panel::<AssistantPanel>(cx) {
 491            if let Some(editor) = workspace
 492                .active_item(cx)
 493                .and_then(|item| item.downcast::<Editor>())
 494            {
 495                let handled = panel.update(cx, |panel, cx| {
 496                    if let Some(assist_id) = panel
 497                        .pending_inline_assist_ids_by_editor
 498                        .get(&editor.downgrade())
 499                        .and_then(|assist_ids| assist_ids.last().copied())
 500                    {
 501                        panel.finish_inline_assist(assist_id, true, cx);
 502                        true
 503                    } else {
 504                        false
 505                    }
 506                });
 507                if handled {
 508                    return;
 509                }
 510            }
 511        }
 512
 513        cx.propagate();
 514    }
 515
 516    fn finish_inline_assist(&mut self, assist_id: usize, undo: bool, cx: &mut ViewContext<Self>) {
 517        self.hide_inline_assist(assist_id, cx);
 518
 519        if let Some(pending_assist) = self.pending_inline_assists.remove(&assist_id) {
 520            if let hash_map::Entry::Occupied(mut entry) = self
 521                .pending_inline_assist_ids_by_editor
 522                .entry(pending_assist.editor.clone())
 523            {
 524                entry.get_mut().retain(|id| *id != assist_id);
 525                if entry.get().is_empty() {
 526                    entry.remove();
 527                }
 528            }
 529
 530            if let Some(editor) = pending_assist.editor.upgrade() {
 531                self.update_highlights_for_editor(&editor, cx);
 532
 533                if undo {
 534                    pending_assist
 535                        .codegen
 536                        .update(cx, |codegen, cx| codegen.undo(cx));
 537                }
 538            }
 539        }
 540    }
 541
 542    fn hide_inline_assist(&mut self, assist_id: usize, cx: &mut ViewContext<Self>) {
 543        if let Some(pending_assist) = self.pending_inline_assists.get_mut(&assist_id) {
 544            if let Some(editor) = pending_assist.editor.upgrade() {
 545                if let Some((block_id, inline_assistant)) = pending_assist.inline_assistant.take() {
 546                    editor.update(cx, |editor, cx| {
 547                        editor.remove_blocks(HashSet::from_iter([block_id]), None, cx);
 548                        if inline_assistant.focus_handle(cx).contains_focused(cx) {
 549                            editor.focus(cx);
 550                        }
 551                    });
 552                }
 553            }
 554        }
 555    }
 556
 557    fn confirm_inline_assist(
 558        &mut self,
 559        inline_assist_id: usize,
 560        user_prompt: &str,
 561        include_conversation: bool,
 562        cx: &mut ViewContext<Self>,
 563        retrieve_context: bool,
 564    ) {
 565        let conversation = if include_conversation {
 566            self.active_editor()
 567                .map(|editor| editor.read(cx).conversation.clone())
 568        } else {
 569            None
 570        };
 571
 572        let pending_assist =
 573            if let Some(pending_assist) = self.pending_inline_assists.get_mut(&inline_assist_id) {
 574                pending_assist
 575            } else {
 576                return;
 577            };
 578
 579        let editor = if let Some(editor) = pending_assist.editor.upgrade() {
 580            editor
 581        } else {
 582            return;
 583        };
 584
 585        let project = pending_assist.project.clone();
 586
 587        let project_name = if let Some(project) = project.upgrade() {
 588            Some(
 589                project
 590                    .read(cx)
 591                    .worktree_root_names(cx)
 592                    .collect::<Vec<&str>>()
 593                    .join("/"),
 594            )
 595        } else {
 596            None
 597        };
 598
 599        self.inline_prompt_history
 600            .retain(|prompt| prompt != user_prompt);
 601        self.inline_prompt_history.push_back(user_prompt.into());
 602        if self.inline_prompt_history.len() > Self::INLINE_PROMPT_HISTORY_MAX_LEN {
 603            self.inline_prompt_history.pop_front();
 604        }
 605
 606        let codegen = pending_assist.codegen.clone();
 607        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
 608        let range = codegen.read(cx).range();
 609        let start = snapshot.point_to_buffer_offset(range.start);
 610        let end = snapshot.point_to_buffer_offset(range.end);
 611        let (buffer, range) = if let Some((start, end)) = start.zip(end) {
 612            let (start_buffer, start_buffer_offset) = start;
 613            let (end_buffer, end_buffer_offset) = end;
 614            if start_buffer.remote_id() == end_buffer.remote_id() {
 615                (start_buffer.clone(), start_buffer_offset..end_buffer_offset)
 616            } else {
 617                self.finish_inline_assist(inline_assist_id, false, cx);
 618                return;
 619            }
 620        } else {
 621            self.finish_inline_assist(inline_assist_id, false, cx);
 622            return;
 623        };
 624
 625        let language = buffer.language_at(range.start);
 626        let language_name = if let Some(language) = language.as_ref() {
 627            if Arc::ptr_eq(language, &language::PLAIN_TEXT) {
 628                None
 629            } else {
 630                Some(language.name())
 631            }
 632        } else {
 633            None
 634        };
 635
 636        // Higher Temperature increases the randomness of model outputs.
 637        // If Markdown or No Language is Known, increase the randomness for more creative output
 638        // If Code, decrease temperature to get more deterministic outputs
 639        let temperature = if let Some(language) = language_name.clone() {
 640            if language.to_string() != "Markdown".to_string() {
 641                0.5
 642            } else {
 643                1.0
 644            }
 645        } else {
 646            1.0
 647        };
 648
 649        let user_prompt = user_prompt.to_string();
 650
 651        let snippets = if retrieve_context {
 652            let Some(project) = project.upgrade() else {
 653                return;
 654            };
 655
 656            let search_results = if let Some(semantic_index) = self.semantic_index.clone() {
 657                let search_results = semantic_index.update(cx, |this, cx| {
 658                    this.search_project(project, user_prompt.to_string(), 10, vec![], vec![], cx)
 659                });
 660
 661                cx.background_executor()
 662                    .spawn(async move { search_results.await.unwrap_or_default() })
 663            } else {
 664                Task::ready(Vec::new())
 665            };
 666
 667            let snippets = cx.spawn(|_, mut cx| async move {
 668                let mut snippets = Vec::new();
 669                for result in search_results.await {
 670                    snippets.push(PromptCodeSnippet::new(
 671                        result.buffer,
 672                        result.range,
 673                        &mut cx,
 674                    )?);
 675                }
 676                anyhow::Ok(snippets)
 677            });
 678            snippets
 679        } else {
 680            Task::ready(Ok(Vec::new()))
 681        };
 682
 683        let mut model = AssistantSettings::get_global(cx)
 684            .default_open_ai_model
 685            .clone();
 686        let model_name = model.full_name();
 687
 688        let prompt = cx.background_executor().spawn(async move {
 689            let snippets = snippets.await?;
 690
 691            let language_name = language_name.as_deref();
 692            generate_content_prompt(
 693                user_prompt,
 694                language_name,
 695                buffer,
 696                range,
 697                snippets,
 698                model_name,
 699                project_name,
 700            )
 701        });
 702
 703        let mut messages = Vec::new();
 704        if let Some(conversation) = conversation {
 705            let conversation = conversation.read(cx);
 706            let buffer = conversation.buffer.read(cx);
 707            messages.extend(
 708                conversation
 709                    .messages(cx)
 710                    .map(|message| message.to_open_ai_message(buffer)),
 711            );
 712            model = conversation.model.clone();
 713        }
 714
 715        cx.spawn(|_, mut cx| async move {
 716            // I Don't know if we want to return a ? here.
 717            let prompt = prompt.await?;
 718
 719            messages.push(RequestMessage {
 720                role: Role::User,
 721                content: prompt,
 722            });
 723
 724            let request = Box::new(OpenAiRequest {
 725                model: model.full_name().into(),
 726                messages,
 727                stream: true,
 728                stop: vec!["|END|>".to_string()],
 729                temperature,
 730            });
 731
 732            codegen.update(&mut cx, |codegen, cx| codegen.start(request, cx))?;
 733            anyhow::Ok(())
 734        })
 735        .detach();
 736    }
 737
 738    fn update_highlights_for_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Self>) {
 739        let mut background_ranges = Vec::new();
 740        let mut foreground_ranges = Vec::new();
 741        let empty_inline_assist_ids = Vec::new();
 742        let inline_assist_ids = self
 743            .pending_inline_assist_ids_by_editor
 744            .get(&editor.downgrade())
 745            .unwrap_or(&empty_inline_assist_ids);
 746
 747        for inline_assist_id in inline_assist_ids {
 748            if let Some(pending_assist) = self.pending_inline_assists.get(inline_assist_id) {
 749                let codegen = pending_assist.codegen.read(cx);
 750                background_ranges.push(codegen.range());
 751                foreground_ranges.extend(codegen.last_equal_ranges().iter().cloned());
 752            }
 753        }
 754
 755        let snapshot = editor.read(cx).buffer().read(cx).snapshot(cx);
 756        merge_ranges(&mut background_ranges, &snapshot);
 757        merge_ranges(&mut foreground_ranges, &snapshot);
 758        editor.update(cx, |editor, cx| {
 759            if background_ranges.is_empty() {
 760                editor.clear_background_highlights::<PendingInlineAssist>(cx);
 761            } else {
 762                editor.highlight_background::<PendingInlineAssist>(
 763                    background_ranges,
 764                    |theme| theme.editor_active_line_background, // todo!("use the appropriate color")
 765                    cx,
 766                );
 767            }
 768
 769            if foreground_ranges.is_empty() {
 770                editor.clear_highlights::<PendingInlineAssist>(cx);
 771            } else {
 772                editor.highlight_text::<PendingInlineAssist>(
 773                    foreground_ranges,
 774                    HighlightStyle {
 775                        fade_out: Some(0.6),
 776                        ..Default::default()
 777                    },
 778                    cx,
 779                );
 780            }
 781        });
 782    }
 783
 784    fn build_api_key_editor(&mut self, cx: &mut WindowContext<'_>) {
 785        self.api_key_editor = Some(build_api_key_editor(cx));
 786    }
 787
 788    fn new_conversation(&mut self, cx: &mut ViewContext<Self>) -> View<ConversationEditor> {
 789        let editor = cx.new_view(|cx| {
 790            ConversationEditor::new(
 791                self.completion_provider.clone(),
 792                self.languages.clone(),
 793                self.fs.clone(),
 794                self.workspace.clone(),
 795                cx,
 796            )
 797        });
 798        self.add_conversation(editor.clone(), cx);
 799        editor
 800    }
 801
 802    fn add_conversation(&mut self, editor: View<ConversationEditor>, cx: &mut ViewContext<Self>) {
 803        self.subscriptions
 804            .push(cx.subscribe(&editor, Self::handle_conversation_editor_event));
 805
 806        let conversation = editor.read(cx).conversation.clone();
 807        self.subscriptions
 808            .push(cx.observe(&conversation, |_, _, cx| cx.notify()));
 809
 810        let index = self.editors.len();
 811        self.editors.push(editor);
 812        self.set_active_editor_index(Some(index), cx);
 813    }
 814
 815    fn set_active_editor_index(&mut self, index: Option<usize>, cx: &mut ViewContext<Self>) {
 816        self.prev_active_editor_index = self.active_editor_index;
 817        self.active_editor_index = index;
 818        if let Some(editor) = self.active_editor() {
 819            let editor = editor.read(cx).editor.clone();
 820            self.toolbar.update(cx, |toolbar, cx| {
 821                toolbar.set_active_item(Some(&editor), cx);
 822            });
 823            if self.focus_handle.contains_focused(cx) {
 824                cx.focus_view(&editor);
 825            }
 826        } else {
 827            self.toolbar.update(cx, |toolbar, cx| {
 828                toolbar.set_active_item(None, cx);
 829            });
 830        }
 831
 832        cx.notify();
 833    }
 834
 835    fn handle_conversation_editor_event(
 836        &mut self,
 837        _: View<ConversationEditor>,
 838        event: &ConversationEditorEvent,
 839        cx: &mut ViewContext<Self>,
 840    ) {
 841        match event {
 842            ConversationEditorEvent::TabContentChanged => cx.notify(),
 843        }
 844    }
 845
 846    fn save_credentials(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 847        if let Some(api_key) = self
 848            .api_key_editor
 849            .as_ref()
 850            .map(|editor| editor.read(cx).text(cx))
 851        {
 852            if !api_key.is_empty() {
 853                let credential = ProviderCredential::Credentials {
 854                    api_key: api_key.clone(),
 855                };
 856
 857                let completion_provider = self.completion_provider.clone();
 858                cx.spawn(|this, mut cx| async move {
 859                    cx.update(|cx| completion_provider.save_credentials(cx, credential))?
 860                        .await;
 861
 862                    this.update(&mut cx, |this, cx| {
 863                        this.api_key_editor.take();
 864                        this.focus_handle.focus(cx);
 865                        cx.notify();
 866                    })
 867                })
 868                .detach_and_log_err(cx);
 869            }
 870        } else {
 871            cx.propagate();
 872        }
 873    }
 874
 875    fn reset_credentials(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
 876        let completion_provider = self.completion_provider.clone();
 877        cx.spawn(|this, mut cx| async move {
 878            cx.update(|cx| completion_provider.delete_credentials(cx))?
 879                .await;
 880            this.update(&mut cx, |this, cx| {
 881                this.build_api_key_editor(cx);
 882                this.focus_handle.focus(cx);
 883                cx.notify();
 884            })
 885        })
 886        .detach_and_log_err(cx);
 887    }
 888
 889    fn toggle_zoom(&mut self, _: &workspace::ToggleZoom, cx: &mut ViewContext<Self>) {
 890        if self.zoomed {
 891            cx.emit(PanelEvent::ZoomOut)
 892        } else {
 893            cx.emit(PanelEvent::ZoomIn)
 894        }
 895    }
 896
 897    fn deploy(&mut self, action: &search::buffer_search::Deploy, cx: &mut ViewContext<Self>) {
 898        let mut propagate = true;
 899        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
 900            search_bar.update(cx, |search_bar, cx| {
 901                if search_bar.show(cx) {
 902                    search_bar.search_suggested(cx);
 903                    if action.focus {
 904                        let focus_handle = search_bar.focus_handle(cx);
 905                        search_bar.select_query(cx);
 906                        cx.focus(&focus_handle);
 907                    }
 908                    propagate = false
 909                }
 910            });
 911        }
 912        if propagate {
 913            cx.propagate();
 914        }
 915    }
 916
 917    fn handle_editor_cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
 918        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
 919            if !search_bar.read(cx).is_dismissed() {
 920                search_bar.update(cx, |search_bar, cx| {
 921                    search_bar.dismiss(&Default::default(), cx)
 922                });
 923                return;
 924            }
 925        }
 926        cx.propagate();
 927    }
 928
 929    fn select_next_match(&mut self, _: &search::SelectNextMatch, cx: &mut ViewContext<Self>) {
 930        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
 931            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Next, 1, cx));
 932        }
 933    }
 934
 935    fn select_prev_match(&mut self, _: &search::SelectPrevMatch, cx: &mut ViewContext<Self>) {
 936        if let Some(search_bar) = self.toolbar.read(cx).item_of_type::<BufferSearchBar>() {
 937            search_bar.update(cx, |bar, cx| bar.select_match(Direction::Prev, 1, cx));
 938        }
 939    }
 940
 941    fn active_editor(&self) -> Option<&View<ConversationEditor>> {
 942        self.editors.get(self.active_editor_index?)
 943    }
 944
 945    fn render_api_key_editor(
 946        &self,
 947        editor: &View<Editor>,
 948        cx: &mut ViewContext<Self>,
 949    ) -> impl IntoElement {
 950        let settings = ThemeSettings::get_global(cx);
 951        let text_style = TextStyle {
 952            color: if editor.read(cx).read_only(cx) {
 953                cx.theme().colors().text_disabled
 954            } else {
 955                cx.theme().colors().text
 956            },
 957            font_family: settings.ui_font.family.clone(),
 958            font_features: settings.ui_font.features,
 959            font_size: rems(0.875).into(),
 960            font_weight: FontWeight::NORMAL,
 961            font_style: FontStyle::Normal,
 962            line_height: relative(1.3).into(),
 963            background_color: None,
 964            underline: None,
 965            white_space: WhiteSpace::Normal,
 966        };
 967        EditorElement::new(
 968            &editor,
 969            EditorStyle {
 970                background: cx.theme().colors().editor_background,
 971                local_player: cx.theme().players().local(),
 972                text: text_style,
 973                ..Default::default()
 974            },
 975        )
 976    }
 977
 978    fn render_hamburger_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
 979        IconButton::new("hamburger_button", IconName::Menu)
 980            .on_click(cx.listener(|this, _event, cx| {
 981                if this.active_editor().is_some() {
 982                    this.set_active_editor_index(None, cx);
 983                } else {
 984                    this.set_active_editor_index(this.prev_active_editor_index, cx);
 985                }
 986            }))
 987            .tooltip(|cx| Tooltip::text("Conversation History", cx))
 988    }
 989
 990    fn render_editor_tools(&self, cx: &mut ViewContext<Self>) -> Vec<AnyElement> {
 991        if self.active_editor().is_some() {
 992            vec![
 993                Self::render_split_button(cx).into_any_element(),
 994                Self::render_quote_button(cx).into_any_element(),
 995                Self::render_assist_button(cx).into_any_element(),
 996            ]
 997        } else {
 998            Default::default()
 999        }
1000    }
1001
1002    fn render_split_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
1003        IconButton::new("split_button", IconName::Snip)
1004            .on_click(cx.listener(|this, _event, cx| {
1005                if let Some(active_editor) = this.active_editor() {
1006                    active_editor.update(cx, |editor, cx| editor.split(&Default::default(), cx));
1007                }
1008            }))
1009            .icon_size(IconSize::Small)
1010            .tooltip(|cx| Tooltip::for_action("Split Message", &Split, cx))
1011    }
1012
1013    fn render_assist_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
1014        IconButton::new("assist_button", IconName::MagicWand)
1015            .on_click(cx.listener(|this, _event, cx| {
1016                if let Some(active_editor) = this.active_editor() {
1017                    active_editor.update(cx, |editor, cx| editor.assist(&Default::default(), cx));
1018                }
1019            }))
1020            .icon_size(IconSize::Small)
1021            .tooltip(|cx| Tooltip::for_action("Assist", &Assist, cx))
1022    }
1023
1024    fn render_quote_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
1025        IconButton::new("quote_button", IconName::Quote)
1026            .on_click(cx.listener(|this, _event, cx| {
1027                if let Some(workspace) = this.workspace.upgrade() {
1028                    cx.window_context().defer(move |cx| {
1029                        workspace.update(cx, |workspace, cx| {
1030                            ConversationEditor::quote_selection(workspace, &Default::default(), cx)
1031                        });
1032                    });
1033                }
1034            }))
1035            .icon_size(IconSize::Small)
1036            .tooltip(|cx| Tooltip::for_action("Quote Selection", &QuoteSelection, cx))
1037    }
1038
1039    fn render_plus_button(cx: &mut ViewContext<Self>) -> impl IntoElement {
1040        IconButton::new("plus_button", IconName::Plus)
1041            .on_click(cx.listener(|this, _event, cx| {
1042                this.new_conversation(cx);
1043            }))
1044            .icon_size(IconSize::Small)
1045            .tooltip(|cx| Tooltip::for_action("New Conversation", &NewConversation, cx))
1046    }
1047
1048    fn render_zoom_button(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1049        let zoomed = self.zoomed;
1050        IconButton::new("zoom_button", IconName::Maximize)
1051            .on_click(cx.listener(|this, _event, cx| {
1052                this.toggle_zoom(&ToggleZoom, cx);
1053            }))
1054            .selected(zoomed)
1055            .selected_icon(IconName::Minimize)
1056            .icon_size(IconSize::Small)
1057            .tooltip(move |cx| {
1058                Tooltip::for_action(if zoomed { "Zoom Out" } else { "Zoom In" }, &ToggleZoom, cx)
1059            })
1060    }
1061
1062    fn render_saved_conversation(
1063        &mut self,
1064        index: usize,
1065        cx: &mut ViewContext<Self>,
1066    ) -> impl IntoElement {
1067        let conversation = &self.saved_conversations[index];
1068        let path = conversation.path.clone();
1069
1070        ButtonLike::new(index)
1071            .on_click(cx.listener(move |this, _, cx| {
1072                this.open_conversation(path.clone(), cx)
1073                    .detach_and_log_err(cx)
1074            }))
1075            .full_width()
1076            .child(
1077                div()
1078                    .flex()
1079                    .w_full()
1080                    .gap_2()
1081                    .child(
1082                        Label::new(conversation.mtime.format("%F %I:%M%p").to_string())
1083                            .color(Color::Muted)
1084                            .size(LabelSize::Small),
1085                    )
1086                    .child(Label::new(conversation.title.clone()).size(LabelSize::Small)),
1087            )
1088    }
1089
1090    fn open_conversation(&mut self, path: PathBuf, cx: &mut ViewContext<Self>) -> Task<Result<()>> {
1091        cx.focus(&self.focus_handle);
1092
1093        if let Some(ix) = self.editor_index_for_path(&path, cx) {
1094            self.set_active_editor_index(Some(ix), cx);
1095            return Task::ready(Ok(()));
1096        }
1097
1098        let fs = self.fs.clone();
1099        let workspace = self.workspace.clone();
1100        let languages = self.languages.clone();
1101        cx.spawn(|this, mut cx| async move {
1102            let saved_conversation = fs.load(&path).await?;
1103            let saved_conversation = serde_json::from_str(&saved_conversation)?;
1104            let conversation =
1105                Conversation::deserialize(saved_conversation, path.clone(), languages, &mut cx)
1106                    .await?;
1107
1108            this.update(&mut cx, |this, cx| {
1109                // If, by the time we've loaded the conversation, the user has already opened
1110                // the same conversation, we don't want to open it again.
1111                if let Some(ix) = this.editor_index_for_path(&path, cx) {
1112                    this.set_active_editor_index(Some(ix), cx);
1113                } else {
1114                    let editor = cx.new_view(|cx| {
1115                        ConversationEditor::for_conversation(conversation, fs, workspace, cx)
1116                    });
1117                    this.add_conversation(editor, cx);
1118                }
1119            })?;
1120            Ok(())
1121        })
1122    }
1123
1124    fn editor_index_for_path(&self, path: &Path, cx: &AppContext) -> Option<usize> {
1125        self.editors
1126            .iter()
1127            .position(|editor| editor.read(cx).conversation.read(cx).path.as_deref() == Some(path))
1128    }
1129
1130    fn has_credentials(&mut self) -> bool {
1131        self.completion_provider.has_credentials()
1132    }
1133
1134    fn load_credentials(&mut self, cx: &mut ViewContext<Self>) -> Task<()> {
1135        let completion_provider = self.completion_provider.clone();
1136        cx.spawn(|_, mut cx| async move {
1137            if let Some(retrieve_credentials) = cx
1138                .update(|cx| completion_provider.retrieve_credentials(cx))
1139                .log_err()
1140            {
1141                retrieve_credentials.await;
1142            }
1143        })
1144    }
1145}
1146
1147fn build_api_key_editor(cx: &mut WindowContext) -> View<Editor> {
1148    cx.new_view(|cx| {
1149        let mut editor = Editor::single_line(cx);
1150        editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
1151        editor
1152    })
1153}
1154
1155impl Render for AssistantPanel {
1156    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1157        if let Some(api_key_editor) = self.api_key_editor.clone() {
1158            const INSTRUCTIONS: [&'static str; 6] = [
1159                "To use the assistant panel or inline assistant, you need to add your OpenAI API key.",
1160                " - You can create an API key at: platform.openai.com/api-keys",
1161                " - Make sure your OpenAI account has credits",
1162                " - Having a subscription for another service like GitHub Copilot won't work.",
1163                " ",
1164                "Paste your OpenAI API key and press Enter to use the assistant:"
1165            ];
1166
1167            v_flex()
1168                .p_4()
1169                .size_full()
1170                .on_action(cx.listener(AssistantPanel::save_credentials))
1171                .track_focus(&self.focus_handle)
1172                .children(
1173                    INSTRUCTIONS.map(|instruction| Label::new(instruction).size(LabelSize::Small)),
1174                )
1175                .child(
1176                    h_flex()
1177                        .w_full()
1178                        .my_2()
1179                        .px_2()
1180                        .py_1()
1181                        .bg(cx.theme().colors().editor_background)
1182                        .rounded_md()
1183                        .child(self.render_api_key_editor(&api_key_editor, cx)),
1184                )
1185                .child(
1186                    h_flex()
1187                        .gap_2()
1188                        .child(Label::new("Click on").size(LabelSize::Small))
1189                        .child(Icon::new(IconName::Ai).size(IconSize::XSmall))
1190                        .child(
1191                            Label::new("in the status bar to close this panel.")
1192                                .size(LabelSize::Small),
1193                        ),
1194                )
1195        } else {
1196            let header = TabBar::new("assistant_header")
1197                .start_child(
1198                    h_flex().gap_1().child(Self::render_hamburger_button(cx)), // .children(title),
1199                )
1200                .children(self.active_editor().map(|editor| {
1201                    h_flex()
1202                        .h(rems(Tab::CONTAINER_HEIGHT_IN_REMS))
1203                        .flex_1()
1204                        .px_2()
1205                        .child(Label::new(editor.read(cx).title(cx)).into_element())
1206                }))
1207                .when(self.focus_handle.contains_focused(cx), |this| {
1208                    this.end_child(
1209                        h_flex()
1210                            .gap_2()
1211                            .when(self.active_editor().is_some(), |this| {
1212                                this.child(h_flex().gap_1().children(self.render_editor_tools(cx)))
1213                                    .child(
1214                                        ui::Divider::vertical()
1215                                            .inset()
1216                                            .color(ui::DividerColor::Border),
1217                                    )
1218                            })
1219                            .child(
1220                                h_flex()
1221                                    .gap_1()
1222                                    .child(Self::render_plus_button(cx))
1223                                    .child(self.render_zoom_button(cx)),
1224                            ),
1225                    )
1226                });
1227
1228            let contents = if self.active_editor().is_some() {
1229                let mut registrar = DivRegistrar::new(
1230                    |panel, cx| panel.toolbar.read(cx).item_of_type::<BufferSearchBar>(),
1231                    cx,
1232                );
1233                BufferSearchBar::register(&mut registrar);
1234                registrar.into_div()
1235            } else {
1236                div()
1237            };
1238            v_flex()
1239                .key_context("AssistantPanel")
1240                .size_full()
1241                .on_action(cx.listener(|this, _: &workspace::NewFile, cx| {
1242                    this.new_conversation(cx);
1243                }))
1244                .on_action(cx.listener(AssistantPanel::reset_credentials))
1245                .on_action(cx.listener(AssistantPanel::toggle_zoom))
1246                .on_action(cx.listener(AssistantPanel::deploy))
1247                .on_action(cx.listener(AssistantPanel::select_next_match))
1248                .on_action(cx.listener(AssistantPanel::select_prev_match))
1249                .on_action(cx.listener(AssistantPanel::handle_editor_cancel))
1250                .track_focus(&self.focus_handle)
1251                .child(header)
1252                .children(if self.toolbar.read(cx).hidden() {
1253                    None
1254                } else {
1255                    Some(self.toolbar.clone())
1256                })
1257                .child(
1258                    contents
1259                        .flex_1()
1260                        .child(if let Some(editor) = self.active_editor() {
1261                            editor.clone().into_any_element()
1262                        } else {
1263                            let view = cx.view().clone();
1264                            let scroll_handle = self.saved_conversations_scroll_handle.clone();
1265                            let conversation_count = self.saved_conversations.len();
1266                            canvas(move |bounds, cx| {
1267                                uniform_list(
1268                                    view,
1269                                    "saved_conversations",
1270                                    conversation_count,
1271                                    |this, range, cx| {
1272                                        range
1273                                            .map(|ix| this.render_saved_conversation(ix, cx))
1274                                            .collect()
1275                                    },
1276                                )
1277                                .track_scroll(scroll_handle)
1278                                .into_any_element()
1279                                .draw(
1280                                    bounds.origin,
1281                                    bounds.size.map(AvailableSpace::Definite),
1282                                    cx,
1283                                );
1284                            })
1285                            .size_full()
1286                            .into_any_element()
1287                        }),
1288                )
1289        }
1290    }
1291}
1292
1293impl Panel for AssistantPanel {
1294    fn persistent_name() -> &'static str {
1295        "AssistantPanel"
1296    }
1297
1298    fn position(&self, cx: &WindowContext) -> DockPosition {
1299        match AssistantSettings::get_global(cx).dock {
1300            AssistantDockPosition::Left => DockPosition::Left,
1301            AssistantDockPosition::Bottom => DockPosition::Bottom,
1302            AssistantDockPosition::Right => DockPosition::Right,
1303        }
1304    }
1305
1306    fn position_is_valid(&self, _: DockPosition) -> bool {
1307        true
1308    }
1309
1310    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
1311        settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
1312            let dock = match position {
1313                DockPosition::Left => AssistantDockPosition::Left,
1314                DockPosition::Bottom => AssistantDockPosition::Bottom,
1315                DockPosition::Right => AssistantDockPosition::Right,
1316            };
1317            settings.dock = Some(dock);
1318        });
1319    }
1320
1321    fn size(&self, cx: &WindowContext) -> Pixels {
1322        let settings = AssistantSettings::get_global(cx);
1323        match self.position(cx) {
1324            DockPosition::Left | DockPosition::Right => {
1325                self.width.unwrap_or_else(|| settings.default_width)
1326            }
1327            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
1328        }
1329    }
1330
1331    fn set_size(&mut self, size: Option<Pixels>, cx: &mut ViewContext<Self>) {
1332        match self.position(cx) {
1333            DockPosition::Left | DockPosition::Right => self.width = size,
1334            DockPosition::Bottom => self.height = size,
1335        }
1336        cx.notify();
1337    }
1338
1339    fn is_zoomed(&self, _: &WindowContext) -> bool {
1340        self.zoomed
1341    }
1342
1343    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
1344        self.zoomed = zoomed;
1345        cx.notify();
1346    }
1347
1348    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
1349        if active {
1350            let load_credentials = self.load_credentials(cx);
1351            cx.spawn(|this, mut cx| async move {
1352                load_credentials.await;
1353                this.update(&mut cx, |this, cx| {
1354                    if !this.has_credentials() {
1355                        this.build_api_key_editor(cx);
1356                    } else if this.editors.is_empty() {
1357                        this.new_conversation(cx);
1358                    }
1359                })
1360            })
1361            .detach_and_log_err(cx);
1362        }
1363    }
1364
1365    fn icon(&self, cx: &WindowContext) -> Option<IconName> {
1366        Some(IconName::Ai).filter(|_| AssistantSettings::get_global(cx).button)
1367    }
1368
1369    fn icon_tooltip(&self, _cx: &WindowContext) -> Option<&'static str> {
1370        Some("Assistant Panel")
1371    }
1372
1373    fn toggle_action(&self) -> Box<dyn Action> {
1374        Box::new(ToggleFocus)
1375    }
1376}
1377
1378impl EventEmitter<PanelEvent> for AssistantPanel {}
1379
1380impl FocusableView for AssistantPanel {
1381    fn focus_handle(&self, _cx: &AppContext) -> FocusHandle {
1382        self.focus_handle.clone()
1383    }
1384}
1385
1386enum ConversationEvent {
1387    MessagesEdited,
1388    SummaryChanged,
1389    StreamedCompletion,
1390}
1391
1392#[derive(Default)]
1393struct Summary {
1394    text: String,
1395    done: bool,
1396}
1397
1398struct Conversation {
1399    id: Option<String>,
1400    buffer: Model<Buffer>,
1401    message_anchors: Vec<MessageAnchor>,
1402    messages_metadata: HashMap<MessageId, MessageMetadata>,
1403    next_message_id: MessageId,
1404    summary: Option<Summary>,
1405    pending_summary: Task<Option<()>>,
1406    completion_count: usize,
1407    pending_completions: Vec<PendingCompletion>,
1408    model: OpenAiModel,
1409    token_count: Option<usize>,
1410    max_token_count: usize,
1411    pending_token_count: Task<Option<()>>,
1412    pending_save: Task<Result<()>>,
1413    path: Option<PathBuf>,
1414    _subscriptions: Vec<Subscription>,
1415    completion_provider: Arc<dyn CompletionProvider>,
1416}
1417
1418impl EventEmitter<ConversationEvent> for Conversation {}
1419
1420impl Conversation {
1421    fn new(
1422        language_registry: Arc<LanguageRegistry>,
1423        cx: &mut ModelContext<Self>,
1424        completion_provider: Arc<dyn CompletionProvider>,
1425    ) -> Self {
1426        let markdown = language_registry.language_for_name("Markdown");
1427        let buffer = cx.new_model(|cx| {
1428            let mut buffer = Buffer::new(0, BufferId::new(cx.entity_id().as_u64()).unwrap(), "");
1429            buffer.set_language_registry(language_registry);
1430            cx.spawn(|buffer, mut cx| async move {
1431                let markdown = markdown.await?;
1432                buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1433                    buffer.set_language(Some(markdown), cx)
1434                })?;
1435                anyhow::Ok(())
1436            })
1437            .detach_and_log_err(cx);
1438            buffer
1439        });
1440
1441        let settings = AssistantSettings::get_global(cx);
1442        let model = settings.default_open_ai_model.clone();
1443
1444        let mut this = Self {
1445            id: Some(Uuid::new_v4().to_string()),
1446            message_anchors: Default::default(),
1447            messages_metadata: Default::default(),
1448            next_message_id: Default::default(),
1449            summary: None,
1450            pending_summary: Task::ready(None),
1451            completion_count: Default::default(),
1452            pending_completions: Default::default(),
1453            token_count: None,
1454            max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
1455            pending_token_count: Task::ready(None),
1456            model: model.clone(),
1457            _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1458            pending_save: Task::ready(Ok(())),
1459            path: None,
1460            buffer,
1461            completion_provider,
1462        };
1463        let message = MessageAnchor {
1464            id: MessageId(post_inc(&mut this.next_message_id.0)),
1465            start: language::Anchor::MIN,
1466        };
1467        this.message_anchors.push(message.clone());
1468        this.messages_metadata.insert(
1469            message.id,
1470            MessageMetadata {
1471                role: Role::User,
1472                sent_at: Local::now(),
1473                status: MessageStatus::Done,
1474            },
1475        );
1476
1477        this.count_remaining_tokens(cx);
1478        this
1479    }
1480
1481    fn serialize(&self, cx: &AppContext) -> SavedConversation {
1482        SavedConversation {
1483            id: self.id.clone(),
1484            zed: "conversation".into(),
1485            version: SavedConversation::VERSION.into(),
1486            text: self.buffer.read(cx).text(),
1487            message_metadata: self.messages_metadata.clone(),
1488            messages: self
1489                .messages(cx)
1490                .map(|message| SavedMessage {
1491                    id: message.id,
1492                    start: message.offset_range.start,
1493                })
1494                .collect(),
1495            summary: self
1496                .summary
1497                .as_ref()
1498                .map(|summary| summary.text.clone())
1499                .unwrap_or_default(),
1500            model: self.model.clone(),
1501        }
1502    }
1503
1504    async fn deserialize(
1505        saved_conversation: SavedConversation,
1506        path: PathBuf,
1507        language_registry: Arc<LanguageRegistry>,
1508        cx: &mut AsyncAppContext,
1509    ) -> Result<Model<Self>> {
1510        let id = match saved_conversation.id {
1511            Some(id) => Some(id),
1512            None => Some(Uuid::new_v4().to_string()),
1513        };
1514        let model = saved_conversation.model;
1515        let completion_provider: Arc<dyn CompletionProvider> = Arc::new(
1516            OpenAiCompletionProvider::new(
1517                model.full_name().into(),
1518                cx.background_executor().clone(),
1519            )
1520            .await,
1521        );
1522        cx.update(|cx| completion_provider.retrieve_credentials(cx))?
1523            .await;
1524
1525        let markdown = language_registry.language_for_name("Markdown");
1526        let mut message_anchors = Vec::new();
1527        let mut next_message_id = MessageId(0);
1528        let buffer = cx.new_model(|cx| {
1529            let mut buffer = Buffer::new(
1530                0,
1531                BufferId::new(cx.entity_id().as_u64()).unwrap(),
1532                saved_conversation.text,
1533            );
1534            for message in saved_conversation.messages {
1535                message_anchors.push(MessageAnchor {
1536                    id: message.id,
1537                    start: buffer.anchor_before(message.start),
1538                });
1539                next_message_id = cmp::max(next_message_id, MessageId(message.id.0 + 1));
1540            }
1541            buffer.set_language_registry(language_registry);
1542            cx.spawn(|buffer, mut cx| async move {
1543                let markdown = markdown.await?;
1544                buffer.update(&mut cx, |buffer: &mut Buffer, cx| {
1545                    buffer.set_language(Some(markdown), cx)
1546                })?;
1547                anyhow::Ok(())
1548            })
1549            .detach_and_log_err(cx);
1550            buffer
1551        })?;
1552
1553        cx.new_model(|cx| {
1554            let mut this = Self {
1555                id,
1556                message_anchors,
1557                messages_metadata: saved_conversation.message_metadata,
1558                next_message_id,
1559                summary: Some(Summary {
1560                    text: saved_conversation.summary,
1561                    done: true,
1562                }),
1563                pending_summary: Task::ready(None),
1564                completion_count: Default::default(),
1565                pending_completions: Default::default(),
1566                token_count: None,
1567                max_token_count: tiktoken_rs::model::get_context_size(&model.full_name()),
1568                pending_token_count: Task::ready(None),
1569                model,
1570                _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
1571                pending_save: Task::ready(Ok(())),
1572                path: Some(path),
1573                buffer,
1574                completion_provider,
1575            };
1576            this.count_remaining_tokens(cx);
1577            this
1578        })
1579    }
1580
1581    fn handle_buffer_event(
1582        &mut self,
1583        _: Model<Buffer>,
1584        event: &language::Event,
1585        cx: &mut ModelContext<Self>,
1586    ) {
1587        match event {
1588            language::Event::Edited => {
1589                self.count_remaining_tokens(cx);
1590                cx.emit(ConversationEvent::MessagesEdited);
1591            }
1592            _ => {}
1593        }
1594    }
1595
1596    fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
1597        let messages = self
1598            .messages(cx)
1599            .into_iter()
1600            .filter_map(|message| {
1601                Some(tiktoken_rs::ChatCompletionRequestMessage {
1602                    role: match message.role {
1603                        Role::User => "user".into(),
1604                        Role::Assistant => "assistant".into(),
1605                        Role::System => "system".into(),
1606                    },
1607                    content: Some(
1608                        self.buffer
1609                            .read(cx)
1610                            .text_for_range(message.offset_range)
1611                            .collect(),
1612                    ),
1613                    name: None,
1614                    function_call: None,
1615                })
1616            })
1617            .collect::<Vec<_>>();
1618        let model = self.model.clone();
1619        self.pending_token_count = cx.spawn(|this, mut cx| {
1620            async move {
1621                cx.background_executor()
1622                    .timer(Duration::from_millis(200))
1623                    .await;
1624                let token_count = cx
1625                    .background_executor()
1626                    .spawn(async move {
1627                        tiktoken_rs::num_tokens_from_messages(&model.full_name(), &messages)
1628                    })
1629                    .await?;
1630
1631                this.update(&mut cx, |this, cx| {
1632                    this.max_token_count =
1633                        tiktoken_rs::model::get_context_size(&this.model.full_name());
1634                    this.token_count = Some(token_count);
1635                    cx.notify()
1636                })?;
1637                anyhow::Ok(())
1638            }
1639            .log_err()
1640        });
1641    }
1642
1643    fn remaining_tokens(&self) -> Option<isize> {
1644        Some(self.max_token_count as isize - self.token_count? as isize)
1645    }
1646
1647    fn set_model(&mut self, model: OpenAiModel, cx: &mut ModelContext<Self>) {
1648        self.model = model;
1649        self.count_remaining_tokens(cx);
1650        cx.notify();
1651    }
1652
1653    fn assist(
1654        &mut self,
1655        selected_messages: HashSet<MessageId>,
1656        cx: &mut ModelContext<Self>,
1657    ) -> Vec<MessageAnchor> {
1658        let mut user_messages = Vec::new();
1659
1660        let last_message_id = if let Some(last_message_id) =
1661            self.message_anchors.iter().rev().find_map(|message| {
1662                message
1663                    .start
1664                    .is_valid(self.buffer.read(cx))
1665                    .then_some(message.id)
1666            }) {
1667            last_message_id
1668        } else {
1669            return Default::default();
1670        };
1671
1672        let mut should_assist = false;
1673        for selected_message_id in selected_messages {
1674            let selected_message_role =
1675                if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
1676                    metadata.role
1677                } else {
1678                    continue;
1679                };
1680
1681            if selected_message_role == Role::Assistant {
1682                if let Some(user_message) = self.insert_message_after(
1683                    selected_message_id,
1684                    Role::User,
1685                    MessageStatus::Done,
1686                    cx,
1687                ) {
1688                    user_messages.push(user_message);
1689                }
1690            } else {
1691                should_assist = true;
1692            }
1693        }
1694
1695        if should_assist {
1696            if !self.completion_provider.has_credentials() {
1697                log::info!("completion provider has no credentials");
1698                return Default::default();
1699            }
1700
1701            let request: Box<dyn CompletionRequest> = Box::new(OpenAiRequest {
1702                model: self.model.full_name().to_string(),
1703                messages: self
1704                    .messages(cx)
1705                    .filter(|message| matches!(message.status, MessageStatus::Done))
1706                    .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
1707                    .collect(),
1708                stream: true,
1709                stop: vec![],
1710                temperature: 1.0,
1711            });
1712
1713            let stream = self.completion_provider.complete(request);
1714            let assistant_message = self
1715                .insert_message_after(last_message_id, Role::Assistant, MessageStatus::Pending, cx)
1716                .unwrap();
1717
1718            // Queue up the user's next reply.
1719            let user_message = self
1720                .insert_message_after(assistant_message.id, Role::User, MessageStatus::Done, cx)
1721                .unwrap();
1722            user_messages.push(user_message);
1723
1724            let task = cx.spawn({
1725                |this, mut cx| async move {
1726                    let assistant_message_id = assistant_message.id;
1727                    let stream_completion = async {
1728                        let mut messages = stream.await?;
1729
1730                        while let Some(message) = messages.next().await {
1731                            let text = message?;
1732
1733                            this.update(&mut cx, |this, cx| {
1734                                let message_ix = this
1735                                    .message_anchors
1736                                    .iter()
1737                                    .position(|message| message.id == assistant_message_id)?;
1738                                this.buffer.update(cx, |buffer, cx| {
1739                                    let offset = this.message_anchors[message_ix + 1..]
1740                                        .iter()
1741                                        .find(|message| message.start.is_valid(buffer))
1742                                        .map_or(buffer.len(), |message| {
1743                                            message.start.to_offset(buffer).saturating_sub(1)
1744                                        });
1745                                    buffer.edit([(offset..offset, text)], None, cx);
1746                                });
1747                                cx.emit(ConversationEvent::StreamedCompletion);
1748
1749                                Some(())
1750                            })?;
1751                            smol::future::yield_now().await;
1752                        }
1753
1754                        this.update(&mut cx, |this, cx| {
1755                            this.pending_completions
1756                                .retain(|completion| completion.id != this.completion_count);
1757                            this.summarize(cx);
1758                        })?;
1759
1760                        anyhow::Ok(())
1761                    };
1762
1763                    let result = stream_completion.await;
1764
1765                    this.update(&mut cx, |this, cx| {
1766                        if let Some(metadata) =
1767                            this.messages_metadata.get_mut(&assistant_message.id)
1768                        {
1769                            match result {
1770                                Ok(_) => {
1771                                    metadata.status = MessageStatus::Done;
1772                                }
1773                                Err(error) => {
1774                                    metadata.status = MessageStatus::Error(SharedString::from(
1775                                        error.to_string().trim().to_string(),
1776                                    ));
1777                                }
1778                            }
1779                            cx.notify();
1780                        }
1781                    })
1782                    .ok();
1783                }
1784            });
1785
1786            self.pending_completions.push(PendingCompletion {
1787                id: post_inc(&mut self.completion_count),
1788                _task: task,
1789            });
1790        }
1791
1792        user_messages
1793    }
1794
1795    fn cancel_last_assist(&mut self) -> bool {
1796        self.pending_completions.pop().is_some()
1797    }
1798
1799    fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
1800        for id in ids {
1801            if let Some(metadata) = self.messages_metadata.get_mut(&id) {
1802                metadata.role.cycle();
1803                cx.emit(ConversationEvent::MessagesEdited);
1804                cx.notify();
1805            }
1806        }
1807    }
1808
1809    fn insert_message_after(
1810        &mut self,
1811        message_id: MessageId,
1812        role: Role,
1813        status: MessageStatus,
1814        cx: &mut ModelContext<Self>,
1815    ) -> Option<MessageAnchor> {
1816        if let Some(prev_message_ix) = self
1817            .message_anchors
1818            .iter()
1819            .position(|message| message.id == message_id)
1820        {
1821            // Find the next valid message after the one we were given.
1822            let mut next_message_ix = prev_message_ix + 1;
1823            while let Some(next_message) = self.message_anchors.get(next_message_ix) {
1824                if next_message.start.is_valid(self.buffer.read(cx)) {
1825                    break;
1826                }
1827                next_message_ix += 1;
1828            }
1829
1830            let start = self.buffer.update(cx, |buffer, cx| {
1831                let offset = self
1832                    .message_anchors
1833                    .get(next_message_ix)
1834                    .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
1835                buffer.edit([(offset..offset, "\n")], None, cx);
1836                buffer.anchor_before(offset + 1)
1837            });
1838            let message = MessageAnchor {
1839                id: MessageId(post_inc(&mut self.next_message_id.0)),
1840                start,
1841            };
1842            self.message_anchors
1843                .insert(next_message_ix, message.clone());
1844            self.messages_metadata.insert(
1845                message.id,
1846                MessageMetadata {
1847                    role,
1848                    sent_at: Local::now(),
1849                    status,
1850                },
1851            );
1852            cx.emit(ConversationEvent::MessagesEdited);
1853            Some(message)
1854        } else {
1855            None
1856        }
1857    }
1858
1859    fn split_message(
1860        &mut self,
1861        range: Range<usize>,
1862        cx: &mut ModelContext<Self>,
1863    ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
1864        let start_message = self.message_for_offset(range.start, cx);
1865        let end_message = self.message_for_offset(range.end, cx);
1866        if let Some((start_message, end_message)) = start_message.zip(end_message) {
1867            // Prevent splitting when range spans multiple messages.
1868            if start_message.id != end_message.id {
1869                return (None, None);
1870            }
1871
1872            let message = start_message;
1873            let role = message.role;
1874            let mut edited_buffer = false;
1875
1876            let mut suffix_start = None;
1877            if range.start > message.offset_range.start && range.end < message.offset_range.end - 1
1878            {
1879                if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
1880                    suffix_start = Some(range.end + 1);
1881                } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
1882                    suffix_start = Some(range.end);
1883                }
1884            }
1885
1886            let suffix = if let Some(suffix_start) = suffix_start {
1887                MessageAnchor {
1888                    id: MessageId(post_inc(&mut self.next_message_id.0)),
1889                    start: self.buffer.read(cx).anchor_before(suffix_start),
1890                }
1891            } else {
1892                self.buffer.update(cx, |buffer, cx| {
1893                    buffer.edit([(range.end..range.end, "\n")], None, cx);
1894                });
1895                edited_buffer = true;
1896                MessageAnchor {
1897                    id: MessageId(post_inc(&mut self.next_message_id.0)),
1898                    start: self.buffer.read(cx).anchor_before(range.end + 1),
1899                }
1900            };
1901
1902            self.message_anchors
1903                .insert(message.index_range.end + 1, suffix.clone());
1904            self.messages_metadata.insert(
1905                suffix.id,
1906                MessageMetadata {
1907                    role,
1908                    sent_at: Local::now(),
1909                    status: MessageStatus::Done,
1910                },
1911            );
1912
1913            let new_messages =
1914                if range.start == range.end || range.start == message.offset_range.start {
1915                    (None, Some(suffix))
1916                } else {
1917                    let mut prefix_end = None;
1918                    if range.start > message.offset_range.start
1919                        && range.end < message.offset_range.end - 1
1920                    {
1921                        if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
1922                            prefix_end = Some(range.start + 1);
1923                        } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
1924                            == Some('\n')
1925                        {
1926                            prefix_end = Some(range.start);
1927                        }
1928                    }
1929
1930                    let selection = if let Some(prefix_end) = prefix_end {
1931                        cx.emit(ConversationEvent::MessagesEdited);
1932                        MessageAnchor {
1933                            id: MessageId(post_inc(&mut self.next_message_id.0)),
1934                            start: self.buffer.read(cx).anchor_before(prefix_end),
1935                        }
1936                    } else {
1937                        self.buffer.update(cx, |buffer, cx| {
1938                            buffer.edit([(range.start..range.start, "\n")], None, cx)
1939                        });
1940                        edited_buffer = true;
1941                        MessageAnchor {
1942                            id: MessageId(post_inc(&mut self.next_message_id.0)),
1943                            start: self.buffer.read(cx).anchor_before(range.end + 1),
1944                        }
1945                    };
1946
1947                    self.message_anchors
1948                        .insert(message.index_range.end + 1, selection.clone());
1949                    self.messages_metadata.insert(
1950                        selection.id,
1951                        MessageMetadata {
1952                            role,
1953                            sent_at: Local::now(),
1954                            status: MessageStatus::Done,
1955                        },
1956                    );
1957                    (Some(selection), Some(suffix))
1958                };
1959
1960            if !edited_buffer {
1961                cx.emit(ConversationEvent::MessagesEdited);
1962            }
1963            new_messages
1964        } else {
1965            (None, None)
1966        }
1967    }
1968
1969    fn summarize(&mut self, cx: &mut ModelContext<Self>) {
1970        if self.message_anchors.len() >= 2 && self.summary.is_none() {
1971            if !self.completion_provider.has_credentials() {
1972                return;
1973            }
1974
1975            let messages = self
1976                .messages(cx)
1977                .take(2)
1978                .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
1979                .chain(Some(RequestMessage {
1980                    role: Role::User,
1981                    content: "Summarize the conversation into a short title without punctuation"
1982                        .into(),
1983                }));
1984            let request: Box<dyn CompletionRequest> = Box::new(OpenAiRequest {
1985                model: self.model.full_name().to_string(),
1986                messages: messages.collect(),
1987                stream: true,
1988                stop: vec![],
1989                temperature: 1.0,
1990            });
1991
1992            let stream = self.completion_provider.complete(request);
1993            self.pending_summary = cx.spawn(|this, mut cx| {
1994                async move {
1995                    let mut messages = stream.await?;
1996
1997                    while let Some(message) = messages.next().await {
1998                        let text = message?;
1999                        this.update(&mut cx, |this, cx| {
2000                            this.summary
2001                                .get_or_insert(Default::default())
2002                                .text
2003                                .push_str(&text);
2004                            cx.emit(ConversationEvent::SummaryChanged);
2005                        })?;
2006                    }
2007
2008                    this.update(&mut cx, |this, cx| {
2009                        if let Some(summary) = this.summary.as_mut() {
2010                            summary.done = true;
2011                            cx.emit(ConversationEvent::SummaryChanged);
2012                        }
2013                    })?;
2014
2015                    anyhow::Ok(())
2016                }
2017                .log_err()
2018            });
2019        }
2020    }
2021
2022    fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
2023        self.messages_for_offsets([offset], cx).pop()
2024    }
2025
2026    fn messages_for_offsets(
2027        &self,
2028        offsets: impl IntoIterator<Item = usize>,
2029        cx: &AppContext,
2030    ) -> Vec<Message> {
2031        let mut result = Vec::new();
2032
2033        let mut messages = self.messages(cx).peekable();
2034        let mut offsets = offsets.into_iter().peekable();
2035        let mut current_message = messages.next();
2036        while let Some(offset) = offsets.next() {
2037            // Locate the message that contains the offset.
2038            while current_message.as_ref().map_or(false, |message| {
2039                !message.offset_range.contains(&offset) && messages.peek().is_some()
2040            }) {
2041                current_message = messages.next();
2042            }
2043            let Some(message) = current_message.as_ref() else {
2044                break;
2045            };
2046
2047            // Skip offsets that are in the same message.
2048            while offsets.peek().map_or(false, |offset| {
2049                message.offset_range.contains(offset) || messages.peek().is_none()
2050            }) {
2051                offsets.next();
2052            }
2053
2054            result.push(message.clone());
2055        }
2056        result
2057    }
2058
2059    fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
2060        let buffer = self.buffer.read(cx);
2061        let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
2062        iter::from_fn(move || {
2063            while let Some((start_ix, message_anchor)) = message_anchors.next() {
2064                let metadata = self.messages_metadata.get(&message_anchor.id)?;
2065                let message_start = message_anchor.start.to_offset(buffer);
2066                let mut message_end = None;
2067                let mut end_ix = start_ix;
2068                while let Some((_, next_message)) = message_anchors.peek() {
2069                    if next_message.start.is_valid(buffer) {
2070                        message_end = Some(next_message.start);
2071                        break;
2072                    } else {
2073                        end_ix += 1;
2074                        message_anchors.next();
2075                    }
2076                }
2077                let message_end = message_end
2078                    .unwrap_or(language::Anchor::MAX)
2079                    .to_offset(buffer);
2080                return Some(Message {
2081                    index_range: start_ix..end_ix,
2082                    offset_range: message_start..message_end,
2083                    id: message_anchor.id,
2084                    anchor: message_anchor.start,
2085                    role: metadata.role,
2086                    sent_at: metadata.sent_at,
2087                    status: metadata.status.clone(),
2088                });
2089            }
2090            None
2091        })
2092    }
2093
2094    fn save(
2095        &mut self,
2096        debounce: Option<Duration>,
2097        fs: Arc<dyn Fs>,
2098        cx: &mut ModelContext<Conversation>,
2099    ) {
2100        self.pending_save = cx.spawn(|this, mut cx| async move {
2101            if let Some(debounce) = debounce {
2102                cx.background_executor().timer(debounce).await;
2103            }
2104
2105            let (old_path, summary) = this.read_with(&cx, |this, _| {
2106                let path = this.path.clone();
2107                let summary = if let Some(summary) = this.summary.as_ref() {
2108                    if summary.done {
2109                        Some(summary.text.clone())
2110                    } else {
2111                        None
2112                    }
2113                } else {
2114                    None
2115                };
2116                (path, summary)
2117            })?;
2118
2119            if let Some(summary) = summary {
2120                let conversation = this.read_with(&cx, |this, cx| this.serialize(cx))?;
2121                let path = if let Some(old_path) = old_path {
2122                    old_path
2123                } else {
2124                    let mut discriminant = 1;
2125                    let mut new_path;
2126                    loop {
2127                        new_path = CONVERSATIONS_DIR.join(&format!(
2128                            "{} - {}.zed.json",
2129                            summary.trim(),
2130                            discriminant
2131                        ));
2132                        if fs.is_file(&new_path).await {
2133                            discriminant += 1;
2134                        } else {
2135                            break;
2136                        }
2137                    }
2138                    new_path
2139                };
2140
2141                fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
2142                fs.atomic_write(path.clone(), serde_json::to_string(&conversation).unwrap())
2143                    .await?;
2144                this.update(&mut cx, |this, _| this.path = Some(path))?;
2145            }
2146
2147            Ok(())
2148        });
2149    }
2150}
2151
2152struct PendingCompletion {
2153    id: usize,
2154    _task: Task<()>,
2155}
2156
2157enum ConversationEditorEvent {
2158    TabContentChanged,
2159}
2160
2161#[derive(Copy, Clone, Debug, PartialEq)]
2162struct ScrollPosition {
2163    offset_before_cursor: gpui::Point<f32>,
2164    cursor: Anchor,
2165}
2166
2167struct ConversationEditor {
2168    conversation: Model<Conversation>,
2169    fs: Arc<dyn Fs>,
2170    workspace: WeakView<Workspace>,
2171    editor: View<Editor>,
2172    blocks: HashSet<BlockId>,
2173    scroll_position: Option<ScrollPosition>,
2174    _subscriptions: Vec<Subscription>,
2175}
2176
2177impl ConversationEditor {
2178    fn new(
2179        completion_provider: Arc<dyn CompletionProvider>,
2180        language_registry: Arc<LanguageRegistry>,
2181        fs: Arc<dyn Fs>,
2182        workspace: WeakView<Workspace>,
2183        cx: &mut ViewContext<Self>,
2184    ) -> Self {
2185        let conversation =
2186            cx.new_model(|cx| Conversation::new(language_registry, cx, completion_provider));
2187        Self::for_conversation(conversation, fs, workspace, cx)
2188    }
2189
2190    fn for_conversation(
2191        conversation: Model<Conversation>,
2192        fs: Arc<dyn Fs>,
2193        workspace: WeakView<Workspace>,
2194        cx: &mut ViewContext<Self>,
2195    ) -> Self {
2196        let editor = cx.new_view(|cx| {
2197            let mut editor = Editor::for_buffer(conversation.read(cx).buffer.clone(), None, cx);
2198            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
2199            editor.set_show_gutter(false, cx);
2200            editor.set_show_wrap_guides(false, cx);
2201            editor
2202        });
2203
2204        let _subscriptions = vec![
2205            cx.observe(&conversation, |_, _, cx| cx.notify()),
2206            cx.subscribe(&conversation, Self::handle_conversation_event),
2207            cx.subscribe(&editor, Self::handle_editor_event),
2208        ];
2209
2210        let mut this = Self {
2211            conversation,
2212            editor,
2213            blocks: Default::default(),
2214            scroll_position: None,
2215            fs,
2216            workspace,
2217            _subscriptions,
2218        };
2219        this.update_message_headers(cx);
2220        this
2221    }
2222
2223    fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
2224        report_assistant_event(
2225            self.workspace.clone(),
2226            self.conversation.read(cx).id.clone(),
2227            AssistantKind::Panel,
2228            cx,
2229        );
2230
2231        let cursors = self.cursors(cx);
2232
2233        let user_messages = self.conversation.update(cx, |conversation, cx| {
2234            let selected_messages = conversation
2235                .messages_for_offsets(cursors, cx)
2236                .into_iter()
2237                .map(|message| message.id)
2238                .collect();
2239            conversation.assist(selected_messages, cx)
2240        });
2241        let new_selections = user_messages
2242            .iter()
2243            .map(|message| {
2244                let cursor = message
2245                    .start
2246                    .to_offset(self.conversation.read(cx).buffer.read(cx));
2247                cursor..cursor
2248            })
2249            .collect::<Vec<_>>();
2250        if !new_selections.is_empty() {
2251            self.editor.update(cx, |editor, cx| {
2252                editor.change_selections(
2253                    Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
2254                    cx,
2255                    |selections| selections.select_ranges(new_selections),
2256                );
2257            });
2258            // Avoid scrolling to the new cursor position so the assistant's output is stable.
2259            cx.defer(|this, _| this.scroll_position = None);
2260        }
2261    }
2262
2263    fn cancel_last_assist(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2264        if !self
2265            .conversation
2266            .update(cx, |conversation, _| conversation.cancel_last_assist())
2267        {
2268            cx.propagate();
2269        }
2270    }
2271
2272    fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
2273        let cursors = self.cursors(cx);
2274        self.conversation.update(cx, |conversation, cx| {
2275            let messages = conversation
2276                .messages_for_offsets(cursors, cx)
2277                .into_iter()
2278                .map(|message| message.id)
2279                .collect();
2280            conversation.cycle_message_roles(messages, cx)
2281        });
2282    }
2283
2284    fn cursors(&self, cx: &AppContext) -> Vec<usize> {
2285        let selections = self.editor.read(cx).selections.all::<usize>(cx);
2286        selections
2287            .into_iter()
2288            .map(|selection| selection.head())
2289            .collect()
2290    }
2291
2292    fn handle_conversation_event(
2293        &mut self,
2294        _: Model<Conversation>,
2295        event: &ConversationEvent,
2296        cx: &mut ViewContext<Self>,
2297    ) {
2298        match event {
2299            ConversationEvent::MessagesEdited => {
2300                self.update_message_headers(cx);
2301                self.conversation.update(cx, |conversation, cx| {
2302                    conversation.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
2303                });
2304            }
2305            ConversationEvent::SummaryChanged => {
2306                cx.emit(ConversationEditorEvent::TabContentChanged);
2307                self.conversation.update(cx, |conversation, cx| {
2308                    conversation.save(None, self.fs.clone(), cx);
2309                });
2310            }
2311            ConversationEvent::StreamedCompletion => {
2312                self.editor.update(cx, |editor, cx| {
2313                    if let Some(scroll_position) = self.scroll_position {
2314                        let snapshot = editor.snapshot(cx);
2315                        let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
2316                        let scroll_top =
2317                            cursor_point.row() as f32 - scroll_position.offset_before_cursor.y;
2318                        editor.set_scroll_position(
2319                            point(scroll_position.offset_before_cursor.x, scroll_top),
2320                            cx,
2321                        );
2322                    }
2323                });
2324            }
2325        }
2326    }
2327
2328    fn handle_editor_event(
2329        &mut self,
2330        _: View<Editor>,
2331        event: &EditorEvent,
2332        cx: &mut ViewContext<Self>,
2333    ) {
2334        match event {
2335            EditorEvent::ScrollPositionChanged { autoscroll, .. } => {
2336                let cursor_scroll_position = self.cursor_scroll_position(cx);
2337                if *autoscroll {
2338                    self.scroll_position = cursor_scroll_position;
2339                } else if self.scroll_position != cursor_scroll_position {
2340                    self.scroll_position = None;
2341                }
2342            }
2343            EditorEvent::SelectionsChanged { .. } => {
2344                self.scroll_position = self.cursor_scroll_position(cx);
2345            }
2346            _ => {}
2347        }
2348    }
2349
2350    fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
2351        self.editor.update(cx, |editor, cx| {
2352            let snapshot = editor.snapshot(cx);
2353            let cursor = editor.selections.newest_anchor().head();
2354            let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32;
2355            let scroll_position = editor
2356                .scroll_manager
2357                .anchor()
2358                .scroll_position(&snapshot.display_snapshot);
2359
2360            let scroll_bottom = scroll_position.y + editor.visible_line_count().unwrap_or(0.);
2361            if (scroll_position.y..scroll_bottom).contains(&cursor_row) {
2362                Some(ScrollPosition {
2363                    cursor,
2364                    offset_before_cursor: point(scroll_position.x, cursor_row - scroll_position.y),
2365                })
2366            } else {
2367                None
2368            }
2369        })
2370    }
2371
2372    fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
2373        self.editor.update(cx, |editor, cx| {
2374            let buffer = editor.buffer().read(cx).snapshot(cx);
2375            let excerpt_id = *buffer.as_singleton().unwrap().0;
2376            let old_blocks = std::mem::take(&mut self.blocks);
2377            let new_blocks = self
2378                .conversation
2379                .read(cx)
2380                .messages(cx)
2381                .map(|message| BlockProperties {
2382                    position: buffer.anchor_in_excerpt(excerpt_id, message.anchor),
2383                    height: 2,
2384                    style: BlockStyle::Sticky,
2385                    render: Arc::new({
2386                        let conversation = self.conversation.clone();
2387                        move |_cx| {
2388                            let message_id = message.id;
2389                            let sender = ButtonLike::new("role")
2390                                .style(ButtonStyle::Filled)
2391                                .child(match message.role {
2392                                    Role::User => Label::new("You").color(Color::Default),
2393                                    Role::Assistant => Label::new("Assistant").color(Color::Info),
2394                                    Role::System => Label::new("System").color(Color::Warning),
2395                                })
2396                                .tooltip(|cx| {
2397                                    Tooltip::with_meta(
2398                                        "Toggle message role",
2399                                        None,
2400                                        "Available roles: You (User), Assistant, System",
2401                                        cx,
2402                                    )
2403                                })
2404                                .on_click({
2405                                    let conversation = conversation.clone();
2406                                    move |_, cx| {
2407                                        conversation.update(cx, |conversation, cx| {
2408                                            conversation.cycle_message_roles(
2409                                                HashSet::from_iter(Some(message_id)),
2410                                                cx,
2411                                            )
2412                                        })
2413                                    }
2414                                });
2415
2416                            h_flex()
2417                                .id(("message_header", message_id.0))
2418                                .h_11()
2419                                .relative()
2420                                .gap_1()
2421                                .child(sender)
2422                                // TODO: Only show this if the message if the message has been sent
2423                                .child(
2424                                    Label::new(
2425                                        FormatDistance::from_now(DateTimeType::Local(
2426                                            message.sent_at,
2427                                        ))
2428                                        .hide_prefix(true)
2429                                        .add_suffix(true)
2430                                        .to_string(),
2431                                    )
2432                                    .size(LabelSize::XSmall)
2433                                    .color(Color::Muted),
2434                                )
2435                                .children(
2436                                    if let MessageStatus::Error(error) = message.status.clone() {
2437                                        Some(
2438                                            div()
2439                                                .id("error")
2440                                                .tooltip(move |cx| Tooltip::text(error.clone(), cx))
2441                                                .child(Icon::new(IconName::XCircle)),
2442                                        )
2443                                    } else {
2444                                        None
2445                                    },
2446                                )
2447                                .into_any_element()
2448                        }
2449                    }),
2450                    disposition: BlockDisposition::Above,
2451                })
2452                .collect::<Vec<_>>();
2453
2454            editor.remove_blocks(old_blocks, None, cx);
2455            let ids = editor.insert_blocks(new_blocks, None, cx);
2456            self.blocks = HashSet::from_iter(ids);
2457        });
2458    }
2459
2460    fn quote_selection(
2461        workspace: &mut Workspace,
2462        _: &QuoteSelection,
2463        cx: &mut ViewContext<Workspace>,
2464    ) {
2465        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
2466            return;
2467        };
2468        let Some(editor) = workspace
2469            .active_item(cx)
2470            .and_then(|item| item.act_as::<Editor>(cx))
2471        else {
2472            return;
2473        };
2474
2475        let editor = editor.read(cx);
2476        let range = editor.selections.newest::<usize>(cx).range();
2477        let buffer = editor.buffer().read(cx).snapshot(cx);
2478        let start_language = buffer.language_at(range.start);
2479        let end_language = buffer.language_at(range.end);
2480        let language_name = if start_language == end_language {
2481            start_language.map(|language| language.name())
2482        } else {
2483            None
2484        };
2485        let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
2486
2487        let selected_text = buffer.text_for_range(range).collect::<String>();
2488        let text = if selected_text.is_empty() {
2489            None
2490        } else {
2491            Some(if language_name == "markdown" {
2492                selected_text
2493                    .lines()
2494                    .map(|line| format!("> {}", line))
2495                    .collect::<Vec<_>>()
2496                    .join("\n")
2497            } else {
2498                format!("```{language_name}\n{selected_text}\n```")
2499            })
2500        };
2501
2502        // Activate the panel
2503        if !panel.focus_handle(cx).contains_focused(cx) {
2504            workspace.toggle_panel_focus::<AssistantPanel>(cx);
2505        }
2506
2507        if let Some(text) = text {
2508            panel.update(cx, |panel, cx| {
2509                let conversation = panel
2510                    .active_editor()
2511                    .cloned()
2512                    .unwrap_or_else(|| panel.new_conversation(cx));
2513                conversation.update(cx, |conversation, cx| {
2514                    conversation
2515                        .editor
2516                        .update(cx, |editor, cx| editor.insert(&text, cx))
2517                });
2518            });
2519        }
2520    }
2521
2522    fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
2523        let editor = self.editor.read(cx);
2524        let conversation = self.conversation.read(cx);
2525        if editor.selections.count() == 1 {
2526            let selection = editor.selections.newest::<usize>(cx);
2527            let mut copied_text = String::new();
2528            let mut spanned_messages = 0;
2529            for message in conversation.messages(cx) {
2530                if message.offset_range.start >= selection.range().end {
2531                    break;
2532                } else if message.offset_range.end >= selection.range().start {
2533                    let range = cmp::max(message.offset_range.start, selection.range().start)
2534                        ..cmp::min(message.offset_range.end, selection.range().end);
2535                    if !range.is_empty() {
2536                        spanned_messages += 1;
2537                        write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
2538                        for chunk in conversation.buffer.read(cx).text_for_range(range) {
2539                            copied_text.push_str(&chunk);
2540                        }
2541                        copied_text.push('\n');
2542                    }
2543                }
2544            }
2545
2546            if spanned_messages > 1 {
2547                cx.write_to_clipboard(ClipboardItem::new(copied_text));
2548                return;
2549            }
2550        }
2551
2552        cx.propagate();
2553    }
2554
2555    fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
2556        self.conversation.update(cx, |conversation, cx| {
2557            let selections = self.editor.read(cx).selections.disjoint_anchors();
2558            for selection in selections.into_iter() {
2559                let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
2560                let range = selection
2561                    .map(|endpoint| endpoint.to_offset(&buffer))
2562                    .range();
2563                conversation.split_message(range, cx);
2564            }
2565        });
2566    }
2567
2568    fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
2569        self.conversation.update(cx, |conversation, cx| {
2570            conversation.save(None, self.fs.clone(), cx)
2571        });
2572    }
2573
2574    fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
2575        self.conversation.update(cx, |conversation, cx| {
2576            let new_model = conversation.model.cycle();
2577            conversation.set_model(new_model, cx);
2578        });
2579    }
2580
2581    fn title(&self, cx: &AppContext) -> String {
2582        self.conversation
2583            .read(cx)
2584            .summary
2585            .as_ref()
2586            .map(|summary| summary.text.clone())
2587            .unwrap_or_else(|| "New Conversation".into())
2588    }
2589
2590    fn render_current_model(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
2591        Button::new(
2592            "current_model",
2593            self.conversation.read(cx).model.short_name(),
2594        )
2595        .style(ButtonStyle::Filled)
2596        .tooltip(move |cx| Tooltip::text("Change Model", cx))
2597        .on_click(cx.listener(|this, _, cx| this.cycle_model(cx)))
2598    }
2599
2600    fn render_remaining_tokens(&self, cx: &mut ViewContext<Self>) -> Option<impl IntoElement> {
2601        let remaining_tokens = self.conversation.read(cx).remaining_tokens()?;
2602        let remaining_tokens_color = if remaining_tokens <= 0 {
2603            Color::Error
2604        } else if remaining_tokens <= 500 {
2605            Color::Warning
2606        } else {
2607            Color::Default
2608        };
2609        Some(Label::new(remaining_tokens.to_string()).color(remaining_tokens_color))
2610    }
2611}
2612
2613impl EventEmitter<ConversationEditorEvent> for ConversationEditor {}
2614
2615impl Render for ConversationEditor {
2616    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
2617        div()
2618            .key_context("ConversationEditor")
2619            .capture_action(cx.listener(ConversationEditor::cancel_last_assist))
2620            .capture_action(cx.listener(ConversationEditor::save))
2621            .capture_action(cx.listener(ConversationEditor::copy))
2622            .capture_action(cx.listener(ConversationEditor::cycle_message_role))
2623            .on_action(cx.listener(ConversationEditor::assist))
2624            .on_action(cx.listener(ConversationEditor::split))
2625            .size_full()
2626            .relative()
2627            .child(
2628                div()
2629                    .size_full()
2630                    .pl_4()
2631                    .bg(cx.theme().colors().editor_background)
2632                    .child(self.editor.clone()),
2633            )
2634            .child(
2635                h_flex()
2636                    .absolute()
2637                    .gap_1()
2638                    .top_3()
2639                    .right_5()
2640                    .child(self.render_current_model(cx))
2641                    .children(self.render_remaining_tokens(cx)),
2642            )
2643    }
2644}
2645
2646impl FocusableView for ConversationEditor {
2647    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2648        self.editor.focus_handle(cx)
2649    }
2650}
2651
2652#[derive(Clone, Debug)]
2653struct MessageAnchor {
2654    id: MessageId,
2655    start: language::Anchor,
2656}
2657
2658#[derive(Clone, Debug)]
2659pub struct Message {
2660    offset_range: Range<usize>,
2661    index_range: Range<usize>,
2662    id: MessageId,
2663    anchor: language::Anchor,
2664    role: Role,
2665    sent_at: DateTime<Local>,
2666    status: MessageStatus,
2667}
2668
2669impl Message {
2670    fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage {
2671        let content = buffer
2672            .text_for_range(self.offset_range.clone())
2673            .collect::<String>();
2674        RequestMessage {
2675            role: self.role,
2676            content: content.trim_end().into(),
2677        }
2678    }
2679}
2680
2681enum InlineAssistantEvent {
2682    Confirmed {
2683        prompt: String,
2684        include_conversation: bool,
2685        retrieve_context: bool,
2686    },
2687    Canceled,
2688    Dismissed,
2689    IncludeConversationToggled {
2690        include_conversation: bool,
2691    },
2692    RetrieveContextToggled {
2693        retrieve_context: bool,
2694    },
2695}
2696
2697struct InlineAssistant {
2698    id: usize,
2699    prompt_editor: View<Editor>,
2700    workspace: WeakView<Workspace>,
2701    confirmed: bool,
2702    include_conversation: bool,
2703    measurements: Rc<Cell<BlockMeasurements>>,
2704    prompt_history: VecDeque<String>,
2705    prompt_history_ix: Option<usize>,
2706    pending_prompt: String,
2707    codegen: Model<Codegen>,
2708    _subscriptions: Vec<Subscription>,
2709    retrieve_context: bool,
2710    semantic_index: Option<Model<SemanticIndex>>,
2711    semantic_permissioned: Option<bool>,
2712    project: WeakModel<Project>,
2713    maintain_rate_limit: Option<Task<()>>,
2714}
2715
2716impl EventEmitter<InlineAssistantEvent> for InlineAssistant {}
2717
2718impl Render for InlineAssistant {
2719    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl Element {
2720        let measurements = self.measurements.get();
2721        h_flex()
2722            .w_full()
2723            .py_2()
2724            .border_y_1()
2725            .border_color(cx.theme().colors().border)
2726            .on_action(cx.listener(Self::confirm))
2727            .on_action(cx.listener(Self::cancel))
2728            .on_action(cx.listener(Self::toggle_include_conversation))
2729            .on_action(cx.listener(Self::toggle_retrieve_context))
2730            .on_action(cx.listener(Self::move_up))
2731            .on_action(cx.listener(Self::move_down))
2732            .child(
2733                h_flex()
2734                    .justify_center()
2735                    .w(measurements.gutter_width)
2736                    .child(
2737                        IconButton::new("include_conversation", IconName::Ai)
2738                            .on_click(cx.listener(|this, _, cx| {
2739                                this.toggle_include_conversation(&ToggleIncludeConversation, cx)
2740                            }))
2741                            .selected(self.include_conversation)
2742                            .tooltip(|cx| {
2743                                Tooltip::for_action(
2744                                    "Include Conversation",
2745                                    &ToggleIncludeConversation,
2746                                    cx,
2747                                )
2748                            }),
2749                    )
2750                    .children(if SemanticIndex::enabled(cx) {
2751                        Some(
2752                            IconButton::new("retrieve_context", IconName::MagnifyingGlass)
2753                                .on_click(cx.listener(|this, _, cx| {
2754                                    this.toggle_retrieve_context(&ToggleRetrieveContext, cx)
2755                                }))
2756                                .selected(self.retrieve_context)
2757                                .tooltip(|cx| {
2758                                    Tooltip::for_action(
2759                                        "Retrieve Context",
2760                                        &ToggleRetrieveContext,
2761                                        cx,
2762                                    )
2763                                }),
2764                        )
2765                    } else {
2766                        None
2767                    })
2768                    .children(if let Some(error) = self.codegen.read(cx).error() {
2769                        let error_message = SharedString::from(error.to_string());
2770                        Some(
2771                            div()
2772                                .id("error")
2773                                .tooltip(move |cx| Tooltip::text(error_message.clone(), cx))
2774                                .child(Icon::new(IconName::XCircle).color(Color::Error)),
2775                        )
2776                    } else {
2777                        None
2778                    }),
2779            )
2780            .child(
2781                h_flex()
2782                    .w_full()
2783                    .ml(measurements.anchor_x - measurements.gutter_width)
2784                    .child(self.render_prompt_editor(cx)),
2785            )
2786            .children(if self.retrieve_context {
2787                self.retrieve_context_status(cx)
2788            } else {
2789                None
2790            })
2791    }
2792}
2793
2794impl FocusableView for InlineAssistant {
2795    fn focus_handle(&self, cx: &AppContext) -> FocusHandle {
2796        self.prompt_editor.focus_handle(cx)
2797    }
2798}
2799
2800impl InlineAssistant {
2801    fn new(
2802        id: usize,
2803        measurements: Rc<Cell<BlockMeasurements>>,
2804        include_conversation: bool,
2805        prompt_history: VecDeque<String>,
2806        codegen: Model<Codegen>,
2807        workspace: WeakView<Workspace>,
2808        cx: &mut ViewContext<Self>,
2809        retrieve_context: bool,
2810        semantic_index: Option<Model<SemanticIndex>>,
2811        project: Model<Project>,
2812    ) -> Self {
2813        let prompt_editor = cx.new_view(|cx| {
2814            let mut editor = Editor::single_line(cx);
2815            let placeholder = match codegen.read(cx).kind() {
2816                CodegenKind::Transform { .. } => "Enter transformation prompt…",
2817                CodegenKind::Generate { .. } => "Enter generation prompt…",
2818            };
2819            editor.set_placeholder_text(placeholder, cx);
2820            editor
2821        });
2822        cx.focus_view(&prompt_editor);
2823
2824        let mut subscriptions = vec![
2825            cx.observe(&codegen, Self::handle_codegen_changed),
2826            cx.subscribe(&prompt_editor, Self::handle_prompt_editor_events),
2827        ];
2828
2829        if let Some(semantic_index) = semantic_index.clone() {
2830            subscriptions.push(cx.observe(&semantic_index, Self::semantic_index_changed));
2831        }
2832
2833        let assistant = Self {
2834            id,
2835            prompt_editor,
2836            workspace,
2837            confirmed: false,
2838            include_conversation,
2839            measurements,
2840            prompt_history,
2841            prompt_history_ix: None,
2842            pending_prompt: String::new(),
2843            codegen,
2844            _subscriptions: subscriptions,
2845            retrieve_context,
2846            semantic_permissioned: None,
2847            semantic_index,
2848            project: project.downgrade(),
2849            maintain_rate_limit: None,
2850        };
2851
2852        assistant.index_project(cx).log_err();
2853
2854        assistant
2855    }
2856
2857    fn semantic_permissioned(&self, cx: &mut ViewContext<Self>) -> Task<Result<bool>> {
2858        if let Some(value) = self.semantic_permissioned {
2859            return Task::ready(Ok(value));
2860        }
2861
2862        let Some(project) = self.project.upgrade() else {
2863            return Task::ready(Err(anyhow!("project was dropped")));
2864        };
2865
2866        self.semantic_index
2867            .as_ref()
2868            .map(|semantic| {
2869                semantic.update(cx, |this, cx| this.project_previously_indexed(&project, cx))
2870            })
2871            .unwrap_or(Task::ready(Ok(false)))
2872    }
2873
2874    fn handle_prompt_editor_events(
2875        &mut self,
2876        _: View<Editor>,
2877        event: &EditorEvent,
2878        cx: &mut ViewContext<Self>,
2879    ) {
2880        if let EditorEvent::Edited = event {
2881            self.pending_prompt = self.prompt_editor.read(cx).text(cx);
2882            cx.notify();
2883        }
2884    }
2885
2886    fn semantic_index_changed(
2887        &mut self,
2888        semantic_index: Model<SemanticIndex>,
2889        cx: &mut ViewContext<Self>,
2890    ) {
2891        let Some(project) = self.project.upgrade() else {
2892            return;
2893        };
2894
2895        let status = semantic_index.read(cx).status(&project);
2896        match status {
2897            SemanticIndexStatus::Indexing {
2898                rate_limit_expiry: Some(_),
2899                ..
2900            } => {
2901                if self.maintain_rate_limit.is_none() {
2902                    self.maintain_rate_limit = Some(cx.spawn(|this, mut cx| async move {
2903                        loop {
2904                            cx.background_executor().timer(Duration::from_secs(1)).await;
2905                            this.update(&mut cx, |_, cx| cx.notify()).log_err();
2906                        }
2907                    }));
2908                }
2909                return;
2910            }
2911            _ => {
2912                self.maintain_rate_limit = None;
2913            }
2914        }
2915    }
2916
2917    fn handle_codegen_changed(&mut self, _: Model<Codegen>, cx: &mut ViewContext<Self>) {
2918        let is_read_only = !self.codegen.read(cx).idle();
2919        self.prompt_editor.update(cx, |editor, cx| {
2920            let was_read_only = editor.read_only(cx);
2921            if was_read_only != is_read_only {
2922                if is_read_only {
2923                    editor.set_read_only(true);
2924                } else {
2925                    self.confirmed = false;
2926                    editor.set_read_only(false);
2927                }
2928            }
2929        });
2930        cx.notify();
2931    }
2932
2933    fn cancel(&mut self, _: &editor::actions::Cancel, cx: &mut ViewContext<Self>) {
2934        cx.emit(InlineAssistantEvent::Canceled);
2935    }
2936
2937    fn confirm(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
2938        if self.confirmed {
2939            cx.emit(InlineAssistantEvent::Dismissed);
2940        } else {
2941            report_assistant_event(self.workspace.clone(), None, AssistantKind::Inline, cx);
2942
2943            let prompt = self.prompt_editor.read(cx).text(cx);
2944            self.prompt_editor
2945                .update(cx, |editor, _cx| editor.set_read_only(true));
2946            cx.emit(InlineAssistantEvent::Confirmed {
2947                prompt,
2948                include_conversation: self.include_conversation,
2949                retrieve_context: self.retrieve_context,
2950            });
2951            self.confirmed = true;
2952            cx.notify();
2953        }
2954    }
2955
2956    fn toggle_retrieve_context(&mut self, _: &ToggleRetrieveContext, cx: &mut ViewContext<Self>) {
2957        let semantic_permissioned = self.semantic_permissioned(cx);
2958
2959        let Some(project) = self.project.upgrade() else {
2960            return;
2961        };
2962
2963        let project_name = project
2964            .read(cx)
2965            .worktree_root_names(cx)
2966            .collect::<Vec<&str>>()
2967            .join("/");
2968        let is_plural = project_name.chars().filter(|letter| *letter == '/').count() > 0;
2969        let prompt_text = format!("Would you like to index the '{}' project{} for context retrieval? This requires sending code to the OpenAI API", project_name,
2970            if is_plural {
2971                "s"
2972            } else {""});
2973
2974        cx.spawn(|this, mut cx| async move {
2975            // If Necessary prompt user
2976            if !semantic_permissioned.await.unwrap_or(false) {
2977                let answer = this.update(&mut cx, |_, cx| {
2978                    cx.prompt(
2979                        PromptLevel::Info,
2980                        prompt_text.as_str(),
2981                        None,
2982                        &["Continue", "Cancel"],
2983                    )
2984                })?;
2985
2986                if answer.await? == 0 {
2987                    this.update(&mut cx, |this, _| {
2988                        this.semantic_permissioned = Some(true);
2989                    })?;
2990                } else {
2991                    return anyhow::Ok(());
2992                }
2993            }
2994
2995            // If permissioned, update context appropriately
2996            this.update(&mut cx, |this, cx| {
2997                this.retrieve_context = !this.retrieve_context;
2998
2999                cx.emit(InlineAssistantEvent::RetrieveContextToggled {
3000                    retrieve_context: this.retrieve_context,
3001                });
3002
3003                if this.retrieve_context {
3004                    this.index_project(cx).log_err();
3005                }
3006
3007                cx.notify();
3008            })?;
3009
3010            anyhow::Ok(())
3011        })
3012        .detach_and_log_err(cx);
3013    }
3014
3015    fn index_project(&self, cx: &mut ViewContext<Self>) -> anyhow::Result<()> {
3016        let Some(project) = self.project.upgrade() else {
3017            return Err(anyhow!("project was dropped!"));
3018        };
3019
3020        let semantic_permissioned = self.semantic_permissioned(cx);
3021        if let Some(semantic_index) = SemanticIndex::global(cx) {
3022            cx.spawn(|_, mut cx| async move {
3023                // This has to be updated to accommodate for semantic_permissions
3024                if semantic_permissioned.await.unwrap_or(false) {
3025                    semantic_index
3026                        .update(&mut cx, |index, cx| index.index_project(project, cx))?
3027                        .await
3028                } else {
3029                    Err(anyhow!("project is not permissioned for semantic indexing"))
3030                }
3031            })
3032            .detach_and_log_err(cx);
3033        }
3034
3035        anyhow::Ok(())
3036    }
3037
3038    fn retrieve_context_status(&self, cx: &mut ViewContext<Self>) -> Option<AnyElement> {
3039        let Some(project) = self.project.upgrade() else {
3040            return None;
3041        };
3042
3043        let semantic_index = SemanticIndex::global(cx)?;
3044        let status = semantic_index.update(cx, |index, _| index.status(&project));
3045        match status {
3046            SemanticIndexStatus::NotAuthenticated {} => Some(
3047                div()
3048                    .id("error")
3049                    .tooltip(|cx| Tooltip::text("Not Authenticated. Please ensure you have a valid 'OPENAI_API_KEY' in your environment variables.", cx))
3050                    .child(Icon::new(IconName::XCircle))
3051                    .into_any_element()
3052            ),
3053
3054            SemanticIndexStatus::NotIndexed {} => Some(
3055                div()
3056                    .id("error")
3057                    .tooltip(|cx| Tooltip::text("Not Indexed", cx))
3058                    .child(Icon::new(IconName::XCircle))
3059                    .into_any_element()
3060            ),
3061
3062            SemanticIndexStatus::Indexing {
3063                remaining_files,
3064                rate_limit_expiry,
3065            } => {
3066                let mut status_text = if remaining_files == 0 {
3067                    "Indexing...".to_string()
3068                } else {
3069                    format!("Remaining files to index: {remaining_files}")
3070                };
3071
3072                if let Some(rate_limit_expiry) = rate_limit_expiry {
3073                    let remaining_seconds = rate_limit_expiry.duration_since(Instant::now());
3074                    if remaining_seconds > Duration::from_secs(0) && remaining_files > 0 {
3075                        write!(
3076                            status_text,
3077                            " (rate limit expires in {}s)",
3078                            remaining_seconds.as_secs()
3079                        )
3080                        .unwrap();
3081                    }
3082                }
3083
3084                let status_text = SharedString::from(status_text);
3085                Some(
3086                    div()
3087                        .id("update")
3088                        .tooltip(move |cx| Tooltip::text(status_text.clone(), cx))
3089                        .child(Icon::new(IconName::Update).color(Color::Info))
3090                        .into_any_element()
3091                )
3092            }
3093
3094            SemanticIndexStatus::Indexed {} => Some(
3095                div()
3096                    .id("check")
3097                    .tooltip(|cx| Tooltip::text("Index up to date", cx))
3098                    .child(Icon::new(IconName::Check).color(Color::Success))
3099                    .into_any_element()
3100            ),
3101        }
3102    }
3103
3104    fn toggle_include_conversation(
3105        &mut self,
3106        _: &ToggleIncludeConversation,
3107        cx: &mut ViewContext<Self>,
3108    ) {
3109        self.include_conversation = !self.include_conversation;
3110        cx.emit(InlineAssistantEvent::IncludeConversationToggled {
3111            include_conversation: self.include_conversation,
3112        });
3113        cx.notify();
3114    }
3115
3116    fn move_up(&mut self, _: &MoveUp, cx: &mut ViewContext<Self>) {
3117        if let Some(ix) = self.prompt_history_ix {
3118            if ix > 0 {
3119                self.prompt_history_ix = Some(ix - 1);
3120                let prompt = self.prompt_history[ix - 1].clone();
3121                self.set_prompt(&prompt, cx);
3122            }
3123        } else if !self.prompt_history.is_empty() {
3124            self.prompt_history_ix = Some(self.prompt_history.len() - 1);
3125            let prompt = self.prompt_history[self.prompt_history.len() - 1].clone();
3126            self.set_prompt(&prompt, cx);
3127        }
3128    }
3129
3130    fn move_down(&mut self, _: &MoveDown, cx: &mut ViewContext<Self>) {
3131        if let Some(ix) = self.prompt_history_ix {
3132            if ix < self.prompt_history.len() - 1 {
3133                self.prompt_history_ix = Some(ix + 1);
3134                let prompt = self.prompt_history[ix + 1].clone();
3135                self.set_prompt(&prompt, cx);
3136            } else {
3137                self.prompt_history_ix = None;
3138                let pending_prompt = self.pending_prompt.clone();
3139                self.set_prompt(&pending_prompt, cx);
3140            }
3141        }
3142    }
3143
3144    fn set_prompt(&mut self, prompt: &str, cx: &mut ViewContext<Self>) {
3145        self.prompt_editor.update(cx, |editor, cx| {
3146            editor.buffer().update(cx, |buffer, cx| {
3147                let len = buffer.len(cx);
3148                buffer.edit([(0..len, prompt)], None, cx);
3149            });
3150        });
3151    }
3152
3153    fn render_prompt_editor(&self, cx: &mut ViewContext<Self>) -> impl IntoElement {
3154        let settings = ThemeSettings::get_global(cx);
3155        let text_style = TextStyle {
3156            color: if self.prompt_editor.read(cx).read_only(cx) {
3157                cx.theme().colors().text_disabled
3158            } else {
3159                cx.theme().colors().text
3160            },
3161            font_family: settings.ui_font.family.clone(),
3162            font_features: settings.ui_font.features,
3163            font_size: rems(0.875).into(),
3164            font_weight: FontWeight::NORMAL,
3165            font_style: FontStyle::Normal,
3166            line_height: relative(1.3).into(),
3167            background_color: None,
3168            underline: None,
3169            white_space: WhiteSpace::Normal,
3170        };
3171        EditorElement::new(
3172            &self.prompt_editor,
3173            EditorStyle {
3174                background: cx.theme().colors().editor_background,
3175                local_player: cx.theme().players().local(),
3176                text: text_style,
3177                ..Default::default()
3178            },
3179        )
3180    }
3181}
3182
3183// This wouldn't need to exist if we could pass parameters when rendering child views.
3184#[derive(Copy, Clone, Default)]
3185struct BlockMeasurements {
3186    anchor_x: Pixels,
3187    gutter_width: Pixels,
3188}
3189
3190struct PendingInlineAssist {
3191    editor: WeakView<Editor>,
3192    inline_assistant: Option<(BlockId, View<InlineAssistant>)>,
3193    codegen: Model<Codegen>,
3194    _subscriptions: Vec<Subscription>,
3195    project: WeakModel<Project>,
3196}
3197
3198fn merge_ranges(ranges: &mut Vec<Range<Anchor>>, buffer: &MultiBufferSnapshot) {
3199    ranges.sort_unstable_by(|a, b| {
3200        a.start
3201            .cmp(&b.start, buffer)
3202            .then_with(|| b.end.cmp(&a.end, buffer))
3203    });
3204
3205    let mut ix = 0;
3206    while ix + 1 < ranges.len() {
3207        let b = ranges[ix + 1].clone();
3208        let a = &mut ranges[ix];
3209        if a.end.cmp(&b.start, buffer).is_gt() {
3210            if a.end.cmp(&b.end, buffer).is_lt() {
3211                a.end = b.end;
3212            }
3213            ranges.remove(ix + 1);
3214        } else {
3215            ix += 1;
3216        }
3217    }
3218}
3219
3220#[cfg(test)]
3221mod tests {
3222    use super::*;
3223    use crate::MessageId;
3224    use ai::test::FakeCompletionProvider;
3225    use gpui::{AppContext, TestAppContext};
3226    use settings::SettingsStore;
3227
3228    #[gpui::test]
3229    fn test_inserting_and_removing_messages(cx: &mut AppContext) {
3230        let settings_store = SettingsStore::test(cx);
3231        cx.set_global(settings_store);
3232        init(cx);
3233        let registry = Arc::new(LanguageRegistry::test());
3234
3235        let completion_provider = Arc::new(FakeCompletionProvider::new());
3236        let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
3237        let buffer = conversation.read(cx).buffer.clone();
3238
3239        let message_1 = conversation.read(cx).message_anchors[0].clone();
3240        assert_eq!(
3241            messages(&conversation, cx),
3242            vec![(message_1.id, Role::User, 0..0)]
3243        );
3244
3245        let message_2 = conversation.update(cx, |conversation, cx| {
3246            conversation
3247                .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
3248                .unwrap()
3249        });
3250        assert_eq!(
3251            messages(&conversation, cx),
3252            vec![
3253                (message_1.id, Role::User, 0..1),
3254                (message_2.id, Role::Assistant, 1..1)
3255            ]
3256        );
3257
3258        buffer.update(cx, |buffer, cx| {
3259            buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
3260        });
3261        assert_eq!(
3262            messages(&conversation, cx),
3263            vec![
3264                (message_1.id, Role::User, 0..2),
3265                (message_2.id, Role::Assistant, 2..3)
3266            ]
3267        );
3268
3269        let message_3 = conversation.update(cx, |conversation, cx| {
3270            conversation
3271                .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3272                .unwrap()
3273        });
3274        assert_eq!(
3275            messages(&conversation, cx),
3276            vec![
3277                (message_1.id, Role::User, 0..2),
3278                (message_2.id, Role::Assistant, 2..4),
3279                (message_3.id, Role::User, 4..4)
3280            ]
3281        );
3282
3283        let message_4 = conversation.update(cx, |conversation, cx| {
3284            conversation
3285                .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3286                .unwrap()
3287        });
3288        assert_eq!(
3289            messages(&conversation, cx),
3290            vec![
3291                (message_1.id, Role::User, 0..2),
3292                (message_2.id, Role::Assistant, 2..4),
3293                (message_4.id, Role::User, 4..5),
3294                (message_3.id, Role::User, 5..5),
3295            ]
3296        );
3297
3298        buffer.update(cx, |buffer, cx| {
3299            buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
3300        });
3301        assert_eq!(
3302            messages(&conversation, cx),
3303            vec![
3304                (message_1.id, Role::User, 0..2),
3305                (message_2.id, Role::Assistant, 2..4),
3306                (message_4.id, Role::User, 4..6),
3307                (message_3.id, Role::User, 6..7),
3308            ]
3309        );
3310
3311        // Deleting across message boundaries merges the messages.
3312        buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
3313        assert_eq!(
3314            messages(&conversation, cx),
3315            vec![
3316                (message_1.id, Role::User, 0..3),
3317                (message_3.id, Role::User, 3..4),
3318            ]
3319        );
3320
3321        // Undoing the deletion should also undo the merge.
3322        buffer.update(cx, |buffer, cx| buffer.undo(cx));
3323        assert_eq!(
3324            messages(&conversation, cx),
3325            vec![
3326                (message_1.id, Role::User, 0..2),
3327                (message_2.id, Role::Assistant, 2..4),
3328                (message_4.id, Role::User, 4..6),
3329                (message_3.id, Role::User, 6..7),
3330            ]
3331        );
3332
3333        // Redoing the deletion should also redo the merge.
3334        buffer.update(cx, |buffer, cx| buffer.redo(cx));
3335        assert_eq!(
3336            messages(&conversation, cx),
3337            vec![
3338                (message_1.id, Role::User, 0..3),
3339                (message_3.id, Role::User, 3..4),
3340            ]
3341        );
3342
3343        // Ensure we can still insert after a merged message.
3344        let message_5 = conversation.update(cx, |conversation, cx| {
3345            conversation
3346                .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3347                .unwrap()
3348        });
3349        assert_eq!(
3350            messages(&conversation, cx),
3351            vec![
3352                (message_1.id, Role::User, 0..3),
3353                (message_5.id, Role::System, 3..4),
3354                (message_3.id, Role::User, 4..5)
3355            ]
3356        );
3357    }
3358
3359    #[gpui::test]
3360    fn test_message_splitting(cx: &mut AppContext) {
3361        let settings_store = SettingsStore::test(cx);
3362        cx.set_global(settings_store);
3363        init(cx);
3364        let registry = Arc::new(LanguageRegistry::test());
3365        let completion_provider = Arc::new(FakeCompletionProvider::new());
3366
3367        let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
3368        let buffer = conversation.read(cx).buffer.clone();
3369
3370        let message_1 = conversation.read(cx).message_anchors[0].clone();
3371        assert_eq!(
3372            messages(&conversation, cx),
3373            vec![(message_1.id, Role::User, 0..0)]
3374        );
3375
3376        buffer.update(cx, |buffer, cx| {
3377            buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
3378        });
3379
3380        let (_, message_2) =
3381            conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3382        let message_2 = message_2.unwrap();
3383
3384        // We recycle newlines in the middle of a split message
3385        assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
3386        assert_eq!(
3387            messages(&conversation, cx),
3388            vec![
3389                (message_1.id, Role::User, 0..4),
3390                (message_2.id, Role::User, 4..16),
3391            ]
3392        );
3393
3394        let (_, message_3) =
3395            conversation.update(cx, |conversation, cx| conversation.split_message(3..3, cx));
3396        let message_3 = message_3.unwrap();
3397
3398        // We don't recycle newlines at the end of a split message
3399        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3400        assert_eq!(
3401            messages(&conversation, cx),
3402            vec![
3403                (message_1.id, Role::User, 0..4),
3404                (message_3.id, Role::User, 4..5),
3405                (message_2.id, Role::User, 5..17),
3406            ]
3407        );
3408
3409        let (_, message_4) =
3410            conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3411        let message_4 = message_4.unwrap();
3412        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
3413        assert_eq!(
3414            messages(&conversation, cx),
3415            vec![
3416                (message_1.id, Role::User, 0..4),
3417                (message_3.id, Role::User, 4..5),
3418                (message_2.id, Role::User, 5..9),
3419                (message_4.id, Role::User, 9..17),
3420            ]
3421        );
3422
3423        let (_, message_5) =
3424            conversation.update(cx, |conversation, cx| conversation.split_message(9..9, cx));
3425        let message_5 = message_5.unwrap();
3426        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
3427        assert_eq!(
3428            messages(&conversation, cx),
3429            vec![
3430                (message_1.id, Role::User, 0..4),
3431                (message_3.id, Role::User, 4..5),
3432                (message_2.id, Role::User, 5..9),
3433                (message_4.id, Role::User, 9..10),
3434                (message_5.id, Role::User, 10..18),
3435            ]
3436        );
3437
3438        let (message_6, message_7) = conversation.update(cx, |conversation, cx| {
3439            conversation.split_message(14..16, cx)
3440        });
3441        let message_6 = message_6.unwrap();
3442        let message_7 = message_7.unwrap();
3443        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
3444        assert_eq!(
3445            messages(&conversation, cx),
3446            vec![
3447                (message_1.id, Role::User, 0..4),
3448                (message_3.id, Role::User, 4..5),
3449                (message_2.id, Role::User, 5..9),
3450                (message_4.id, Role::User, 9..10),
3451                (message_5.id, Role::User, 10..14),
3452                (message_6.id, Role::User, 14..17),
3453                (message_7.id, Role::User, 17..19),
3454            ]
3455        );
3456    }
3457
3458    #[gpui::test]
3459    fn test_messages_for_offsets(cx: &mut AppContext) {
3460        let settings_store = SettingsStore::test(cx);
3461        cx.set_global(settings_store);
3462        init(cx);
3463        let registry = Arc::new(LanguageRegistry::test());
3464        let completion_provider = Arc::new(FakeCompletionProvider::new());
3465        let conversation = cx.new_model(|cx| Conversation::new(registry, cx, completion_provider));
3466        let buffer = conversation.read(cx).buffer.clone();
3467
3468        let message_1 = conversation.read(cx).message_anchors[0].clone();
3469        assert_eq!(
3470            messages(&conversation, cx),
3471            vec![(message_1.id, Role::User, 0..0)]
3472        );
3473
3474        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
3475        let message_2 = conversation
3476            .update(cx, |conversation, cx| {
3477                conversation.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
3478            })
3479            .unwrap();
3480        buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
3481
3482        let message_3 = conversation
3483            .update(cx, |conversation, cx| {
3484                conversation.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
3485            })
3486            .unwrap();
3487        buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
3488
3489        assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
3490        assert_eq!(
3491            messages(&conversation, cx),
3492            vec![
3493                (message_1.id, Role::User, 0..4),
3494                (message_2.id, Role::User, 4..8),
3495                (message_3.id, Role::User, 8..11)
3496            ]
3497        );
3498
3499        assert_eq!(
3500            message_ids_for_offsets(&conversation, &[0, 4, 9], cx),
3501            [message_1.id, message_2.id, message_3.id]
3502        );
3503        assert_eq!(
3504            message_ids_for_offsets(&conversation, &[0, 1, 11], cx),
3505            [message_1.id, message_3.id]
3506        );
3507
3508        let message_4 = conversation
3509            .update(cx, |conversation, cx| {
3510                conversation.insert_message_after(message_3.id, Role::User, MessageStatus::Done, cx)
3511            })
3512            .unwrap();
3513        assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\n");
3514        assert_eq!(
3515            messages(&conversation, cx),
3516            vec![
3517                (message_1.id, Role::User, 0..4),
3518                (message_2.id, Role::User, 4..8),
3519                (message_3.id, Role::User, 8..12),
3520                (message_4.id, Role::User, 12..12)
3521            ]
3522        );
3523        assert_eq!(
3524            message_ids_for_offsets(&conversation, &[0, 4, 8, 12], cx),
3525            [message_1.id, message_2.id, message_3.id, message_4.id]
3526        );
3527
3528        fn message_ids_for_offsets(
3529            conversation: &Model<Conversation>,
3530            offsets: &[usize],
3531            cx: &AppContext,
3532        ) -> Vec<MessageId> {
3533            conversation
3534                .read(cx)
3535                .messages_for_offsets(offsets.iter().copied(), cx)
3536                .into_iter()
3537                .map(|message| message.id)
3538                .collect()
3539        }
3540    }
3541
3542    #[gpui::test]
3543    async fn test_serialization(cx: &mut TestAppContext) {
3544        let settings_store = cx.update(SettingsStore::test);
3545        cx.set_global(settings_store);
3546        cx.update(init);
3547        let registry = Arc::new(LanguageRegistry::test());
3548        let completion_provider = Arc::new(FakeCompletionProvider::new());
3549        let conversation =
3550            cx.new_model(|cx| Conversation::new(registry.clone(), cx, completion_provider));
3551        let buffer = conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
3552        let message_0 =
3553            conversation.read_with(cx, |conversation, _| conversation.message_anchors[0].id);
3554        let message_1 = conversation.update(cx, |conversation, cx| {
3555            conversation
3556                .insert_message_after(message_0, Role::Assistant, MessageStatus::Done, cx)
3557                .unwrap()
3558        });
3559        let message_2 = conversation.update(cx, |conversation, cx| {
3560            conversation
3561                .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
3562                .unwrap()
3563        });
3564        buffer.update(cx, |buffer, cx| {
3565            buffer.edit([(0..0, "a"), (1..1, "b\nc")], None, cx);
3566            buffer.finalize_last_transaction();
3567        });
3568        let _message_3 = conversation.update(cx, |conversation, cx| {
3569            conversation
3570                .insert_message_after(message_2.id, Role::System, MessageStatus::Done, cx)
3571                .unwrap()
3572        });
3573        buffer.update(cx, |buffer, cx| buffer.undo(cx));
3574        assert_eq!(buffer.read_with(cx, |buffer, _| buffer.text()), "a\nb\nc\n");
3575        assert_eq!(
3576            cx.read(|cx| messages(&conversation, cx)),
3577            [
3578                (message_0, Role::User, 0..2),
3579                (message_1.id, Role::Assistant, 2..6),
3580                (message_2.id, Role::System, 6..6),
3581            ]
3582        );
3583
3584        let deserialized_conversation = Conversation::deserialize(
3585            conversation.read_with(cx, |conversation, cx| conversation.serialize(cx)),
3586            Default::default(),
3587            registry.clone(),
3588            &mut cx.to_async(),
3589        )
3590        .await
3591        .unwrap();
3592        let deserialized_buffer =
3593            deserialized_conversation.read_with(cx, |conversation, _| conversation.buffer.clone());
3594        assert_eq!(
3595            deserialized_buffer.read_with(cx, |buffer, _| buffer.text()),
3596            "a\nb\nc\n"
3597        );
3598        assert_eq!(
3599            cx.read(|cx| messages(&deserialized_conversation, cx)),
3600            [
3601                (message_0, Role::User, 0..2),
3602                (message_1.id, Role::Assistant, 2..6),
3603                (message_2.id, Role::System, 6..6),
3604            ]
3605        );
3606    }
3607
3608    fn messages(
3609        conversation: &Model<Conversation>,
3610        cx: &AppContext,
3611    ) -> Vec<(MessageId, Role, Range<usize>)> {
3612        conversation
3613            .read(cx)
3614            .messages(cx)
3615            .map(|message| (message.id, message.role, message.offset_range))
3616            .collect()
3617    }
3618}
3619
3620fn report_assistant_event(
3621    workspace: WeakView<Workspace>,
3622    conversation_id: Option<String>,
3623    assistant_kind: AssistantKind,
3624    cx: &AppContext,
3625) {
3626    let Some(workspace) = workspace.upgrade() else {
3627        return;
3628    };
3629
3630    let client = workspace.read(cx).project().read(cx).client();
3631    let telemetry = client.telemetry();
3632
3633    let model = AssistantSettings::get_global(cx)
3634        .default_open_ai_model
3635        .clone();
3636
3637    telemetry.report_assistant_event(conversation_id, assistant_kind, model.full_name())
3638}