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