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