assistant.rs

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