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