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