assistant_panel.rs

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