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