assistant.rs

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