assistant_panel.rs

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