assistant.rs

   1use crate::{
   2    assistant_settings::{AssistantDockPosition, AssistantSettings},
   3    OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role,
   4};
   5use anyhow::{anyhow, Result};
   6use chrono::{DateTime, Local};
   7use collections::{HashMap, HashSet};
   8use editor::{
   9    display_map::{BlockDisposition, BlockId, BlockProperties, BlockStyle, ToDisplayPoint},
  10    scroll::autoscroll::{Autoscroll, AutoscrollStrategy},
  11    Anchor, Editor, ToOffset as _,
  12};
  13use fs::Fs;
  14use futures::{io::BufReader, AsyncBufReadExt, AsyncReadExt, Stream, StreamExt};
  15use gpui::{
  16    actions,
  17    elements::*,
  18    executor::Background,
  19    geometry::vector::{vec2f, Vector2F},
  20    platform::{CursorStyle, MouseButton},
  21    Action, AppContext, AsyncAppContext, ClipboardItem, Entity, ModelContext, ModelHandle,
  22    Subscription, Task, View, ViewContext, ViewHandle, WeakViewHandle, WindowContext,
  23};
  24use isahc::{http::StatusCode, Request, RequestExt};
  25use language::{language_settings::SoftWrap, Buffer, LanguageRegistry, ToOffset as _};
  26use serde::Deserialize;
  27use settings::SettingsStore;
  28use std::{
  29    borrow::Cow, cell::RefCell, cmp, fmt::Write, io, iter, ops::Range, rc::Rc, sync::Arc,
  30    time::Duration,
  31};
  32use util::{channel::ReleaseChannel, post_inc, truncate_and_trailoff, ResultExt, TryFutureExt};
  33use workspace::{
  34    dock::{DockPosition, Panel},
  35    item::Item,
  36    pane, Pane, Workspace,
  37};
  38
  39const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
  40
  41actions!(
  42    assistant,
  43    [NewContext, Assist, QuoteSelection, ToggleFocus, ResetKey]
  44);
  45
  46pub fn init(cx: &mut AppContext) {
  47    if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
  48        cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
  49            filter.filtered_namespaces.insert("assistant");
  50        });
  51    }
  52
  53    settings::register::<AssistantSettings>(cx);
  54    cx.add_action(
  55        |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext<Workspace>| {
  56            if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
  57                this.update(cx, |this, cx| this.add_context(cx))
  58            }
  59
  60            workspace.focus_panel::<AssistantPanel>(cx);
  61        },
  62    );
  63    cx.add_action(AssistantEditor::assist);
  64    cx.capture_action(AssistantEditor::cancel_last_assist);
  65    cx.add_action(AssistantEditor::quote_selection);
  66    cx.capture_action(AssistantEditor::copy);
  67    cx.add_action(AssistantPanel::save_api_key);
  68    cx.add_action(AssistantPanel::reset_api_key);
  69    cx.add_action(
  70        |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
  71            workspace.toggle_panel_focus::<AssistantPanel>(cx);
  72        },
  73    );
  74}
  75
  76pub enum AssistantPanelEvent {
  77    ZoomIn,
  78    ZoomOut,
  79    Focus,
  80    Close,
  81    DockPositionChanged,
  82}
  83
  84pub struct AssistantPanel {
  85    width: Option<f32>,
  86    height: Option<f32>,
  87    pane: ViewHandle<Pane>,
  88    api_key: Rc<RefCell<Option<String>>>,
  89    api_key_editor: Option<ViewHandle<Editor>>,
  90    has_read_credentials: bool,
  91    languages: Arc<LanguageRegistry>,
  92    fs: Arc<dyn Fs>,
  93    subscriptions: Vec<Subscription>,
  94}
  95
  96impl AssistantPanel {
  97    pub fn load(
  98        workspace: WeakViewHandle<Workspace>,
  99        cx: AsyncAppContext,
 100    ) -> Task<Result<ViewHandle<Self>>> {
 101        cx.spawn(|mut cx| async move {
 102            // TODO: deserialize state.
 103            workspace.update(&mut cx, |workspace, cx| {
 104                cx.add_view::<Self, _>(|cx| {
 105                    let weak_self = cx.weak_handle();
 106                    let pane = cx.add_view(|cx| {
 107                        let mut pane = Pane::new(
 108                            workspace.weak_handle(),
 109                            workspace.project().clone(),
 110                            workspace.app_state().background_actions,
 111                            Default::default(),
 112                            cx,
 113                        );
 114                        pane.set_can_split(false, cx);
 115                        pane.set_can_navigate(false, cx);
 116                        pane.on_can_drop(move |_, _| false);
 117                        pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 118                            let weak_self = weak_self.clone();
 119                            Flex::row()
 120                                .with_child(Pane::render_tab_bar_button(
 121                                    0,
 122                                    "icons/plus_12.svg",
 123                                    false,
 124                                    Some(("New Context".into(), Some(Box::new(NewContext)))),
 125                                    cx,
 126                                    move |_, cx| {
 127                                        let weak_self = weak_self.clone();
 128                                        cx.window_context().defer(move |cx| {
 129                                            if let Some(this) = weak_self.upgrade(cx) {
 130                                                this.update(cx, |this, cx| this.add_context(cx));
 131                                            }
 132                                        })
 133                                    },
 134                                    None,
 135                                ))
 136                                .with_child(Pane::render_tab_bar_button(
 137                                    1,
 138                                    if pane.is_zoomed() {
 139                                        "icons/minimize_8.svg"
 140                                    } else {
 141                                        "icons/maximize_8.svg"
 142                                    },
 143                                    pane.is_zoomed(),
 144                                    Some((
 145                                        "Toggle Zoom".into(),
 146                                        Some(Box::new(workspace::ToggleZoom)),
 147                                    )),
 148                                    cx,
 149                                    move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
 150                                    None,
 151                                ))
 152                                .into_any()
 153                        });
 154                        let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
 155                        pane.toolbar()
 156                            .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
 157                        pane
 158                    });
 159
 160                    let mut this = Self {
 161                        pane,
 162                        api_key: Rc::new(RefCell::new(None)),
 163                        api_key_editor: None,
 164                        has_read_credentials: false,
 165                        languages: workspace.app_state().languages.clone(),
 166                        fs: workspace.app_state().fs.clone(),
 167                        width: None,
 168                        height: None,
 169                        subscriptions: Default::default(),
 170                    };
 171
 172                    let mut old_dock_position = this.position(cx);
 173                    this.subscriptions = vec![
 174                        cx.observe(&this.pane, |_, _, cx| cx.notify()),
 175                        cx.subscribe(&this.pane, Self::handle_pane_event),
 176                        cx.observe_global::<SettingsStore, _>(move |this, cx| {
 177                            let new_dock_position = this.position(cx);
 178                            if new_dock_position != old_dock_position {
 179                                old_dock_position = new_dock_position;
 180                                cx.emit(AssistantPanelEvent::DockPositionChanged);
 181                            }
 182                        }),
 183                    ];
 184
 185                    this
 186                })
 187            })
 188        })
 189    }
 190
 191    fn handle_pane_event(
 192        &mut self,
 193        _pane: ViewHandle<Pane>,
 194        event: &pane::Event,
 195        cx: &mut ViewContext<Self>,
 196    ) {
 197        match event {
 198            pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn),
 199            pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut),
 200            pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus),
 201            pane::Event::Remove => cx.emit(AssistantPanelEvent::Close),
 202            _ => {}
 203        }
 204    }
 205
 206    fn add_context(&mut self, cx: &mut ViewContext<Self>) {
 207        let focus = self.has_focus(cx);
 208        let editor = cx
 209            .add_view(|cx| AssistantEditor::new(self.api_key.clone(), self.languages.clone(), cx));
 210        self.subscriptions
 211            .push(cx.subscribe(&editor, Self::handle_assistant_editor_event));
 212        self.pane.update(cx, |pane, cx| {
 213            pane.add_item(Box::new(editor), true, focus, None, cx)
 214        });
 215    }
 216
 217    fn handle_assistant_editor_event(
 218        &mut self,
 219        _: ViewHandle<AssistantEditor>,
 220        event: &AssistantEditorEvent,
 221        cx: &mut ViewContext<Self>,
 222    ) {
 223        match event {
 224            AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()),
 225        }
 226    }
 227
 228    fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 229        if let Some(api_key) = self
 230            .api_key_editor
 231            .as_ref()
 232            .map(|editor| editor.read(cx).text(cx))
 233        {
 234            if !api_key.is_empty() {
 235                cx.platform()
 236                    .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
 237                    .log_err();
 238                *self.api_key.borrow_mut() = Some(api_key);
 239                self.api_key_editor.take();
 240                cx.focus_self();
 241                cx.notify();
 242            }
 243        } else {
 244            cx.propagate_action();
 245        }
 246    }
 247
 248    fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
 249        cx.platform().delete_credentials(OPENAI_API_URL).log_err();
 250        self.api_key.take();
 251        self.api_key_editor = Some(build_api_key_editor(cx));
 252        cx.focus_self();
 253        cx.notify();
 254    }
 255}
 256
 257fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> ViewHandle<Editor> {
 258    cx.add_view(|cx| {
 259        let mut editor = Editor::single_line(
 260            Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())),
 261            cx,
 262        );
 263        editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
 264        editor
 265    })
 266}
 267
 268impl Entity for AssistantPanel {
 269    type Event = AssistantPanelEvent;
 270}
 271
 272impl View for AssistantPanel {
 273    fn ui_name() -> &'static str {
 274        "AssistantPanel"
 275    }
 276
 277    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 278        let style = &theme::current(cx).assistant;
 279        if let Some(api_key_editor) = self.api_key_editor.as_ref() {
 280            Flex::column()
 281                .with_child(
 282                    Text::new(
 283                        "Paste your OpenAI API key and press Enter to use the assistant",
 284                        style.api_key_prompt.text.clone(),
 285                    )
 286                    .aligned(),
 287                )
 288                .with_child(
 289                    ChildView::new(api_key_editor, cx)
 290                        .contained()
 291                        .with_style(style.api_key_editor.container)
 292                        .aligned(),
 293                )
 294                .contained()
 295                .with_style(style.api_key_prompt.container)
 296                .aligned()
 297                .into_any()
 298        } else {
 299            ChildView::new(&self.pane, cx).into_any()
 300        }
 301    }
 302
 303    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
 304        if cx.is_self_focused() {
 305            if let Some(api_key_editor) = self.api_key_editor.as_ref() {
 306                cx.focus(api_key_editor);
 307            } else {
 308                cx.focus(&self.pane);
 309            }
 310        }
 311    }
 312}
 313
 314impl Panel for AssistantPanel {
 315    fn position(&self, cx: &WindowContext) -> DockPosition {
 316        match settings::get::<AssistantSettings>(cx).dock {
 317            AssistantDockPosition::Left => DockPosition::Left,
 318            AssistantDockPosition::Bottom => DockPosition::Bottom,
 319            AssistantDockPosition::Right => DockPosition::Right,
 320        }
 321    }
 322
 323    fn position_is_valid(&self, _: DockPosition) -> bool {
 324        true
 325    }
 326
 327    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
 328        settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
 329            let dock = match position {
 330                DockPosition::Left => AssistantDockPosition::Left,
 331                DockPosition::Bottom => AssistantDockPosition::Bottom,
 332                DockPosition::Right => AssistantDockPosition::Right,
 333            };
 334            settings.dock = Some(dock);
 335        });
 336    }
 337
 338    fn size(&self, cx: &WindowContext) -> f32 {
 339        let settings = settings::get::<AssistantSettings>(cx);
 340        match self.position(cx) {
 341            DockPosition::Left | DockPosition::Right => {
 342                self.width.unwrap_or_else(|| settings.default_width)
 343            }
 344            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
 345        }
 346    }
 347
 348    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
 349        match self.position(cx) {
 350            DockPosition::Left | DockPosition::Right => self.width = Some(size),
 351            DockPosition::Bottom => self.height = Some(size),
 352        }
 353        cx.notify();
 354    }
 355
 356    fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool {
 357        matches!(event, AssistantPanelEvent::ZoomIn)
 358    }
 359
 360    fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool {
 361        matches!(event, AssistantPanelEvent::ZoomOut)
 362    }
 363
 364    fn is_zoomed(&self, cx: &WindowContext) -> bool {
 365        self.pane.read(cx).is_zoomed()
 366    }
 367
 368    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
 369        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
 370    }
 371
 372    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
 373        if active {
 374            if self.api_key.borrow().is_none() && !self.has_read_credentials {
 375                self.has_read_credentials = true;
 376                let api_key = if let Some((_, api_key)) = cx
 377                    .platform()
 378                    .read_credentials(OPENAI_API_URL)
 379                    .log_err()
 380                    .flatten()
 381                {
 382                    String::from_utf8(api_key).log_err()
 383                } else {
 384                    None
 385                };
 386                if let Some(api_key) = api_key {
 387                    *self.api_key.borrow_mut() = Some(api_key);
 388                } else if self.api_key_editor.is_none() {
 389                    self.api_key_editor = Some(build_api_key_editor(cx));
 390                    cx.notify();
 391                }
 392            }
 393
 394            if self.pane.read(cx).items_len() == 0 {
 395                self.add_context(cx);
 396            }
 397        }
 398    }
 399
 400    fn icon_path(&self) -> &'static str {
 401        "icons/robot_14.svg"
 402    }
 403
 404    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
 405        ("Assistant Panel".into(), Some(Box::new(ToggleFocus)))
 406    }
 407
 408    fn should_change_position_on_event(event: &Self::Event) -> bool {
 409        matches!(event, AssistantPanelEvent::DockPositionChanged)
 410    }
 411
 412    fn should_activate_on_event(_: &Self::Event) -> bool {
 413        false
 414    }
 415
 416    fn should_close_on_event(event: &AssistantPanelEvent) -> bool {
 417        matches!(event, AssistantPanelEvent::Close)
 418    }
 419
 420    fn has_focus(&self, cx: &WindowContext) -> bool {
 421        self.pane.read(cx).has_focus()
 422            || self
 423                .api_key_editor
 424                .as_ref()
 425                .map_or(false, |editor| editor.is_focused(cx))
 426    }
 427
 428    fn is_focus_event(event: &Self::Event) -> bool {
 429        matches!(event, AssistantPanelEvent::Focus)
 430    }
 431}
 432
 433enum AssistantEvent {
 434    MessagesEdited,
 435    SummaryChanged,
 436    StreamedCompletion,
 437}
 438
 439struct Assistant {
 440    buffer: ModelHandle<Buffer>,
 441    messages: Vec<Message>,
 442    messages_metadata: HashMap<MessageId, MessageMetadata>,
 443    next_message_id: MessageId,
 444    summary: Option<String>,
 445    pending_summary: Task<Option<()>>,
 446    completion_count: usize,
 447    pending_completions: Vec<PendingCompletion>,
 448    model: String,
 449    token_count: Option<usize>,
 450    max_token_count: usize,
 451    pending_token_count: Task<Option<()>>,
 452    api_key: Rc<RefCell<Option<String>>>,
 453    _subscriptions: Vec<Subscription>,
 454}
 455
 456impl Entity for Assistant {
 457    type Event = AssistantEvent;
 458}
 459
 460impl Assistant {
 461    fn new(
 462        api_key: Rc<RefCell<Option<String>>>,
 463        language_registry: Arc<LanguageRegistry>,
 464        cx: &mut ModelContext<Self>,
 465    ) -> Self {
 466        let model = "gpt-3.5-turbo";
 467        let markdown = language_registry.language_for_name("Markdown");
 468        let buffer = cx.add_model(|cx| {
 469            let mut buffer = Buffer::new(0, "", cx);
 470            buffer.set_language_registry(language_registry);
 471            cx.spawn_weak(|buffer, mut cx| async move {
 472                let markdown = markdown.await?;
 473                let buffer = buffer
 474                    .upgrade(&cx)
 475                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
 476                buffer.update(&mut cx, |buffer, cx| {
 477                    buffer.set_language(Some(markdown), cx)
 478                });
 479                anyhow::Ok(())
 480            })
 481            .detach_and_log_err(cx);
 482            buffer
 483        });
 484
 485        let mut this = Self {
 486            messages: Default::default(),
 487            messages_metadata: Default::default(),
 488            next_message_id: Default::default(),
 489            summary: None,
 490            pending_summary: Task::ready(None),
 491            completion_count: Default::default(),
 492            pending_completions: Default::default(),
 493            token_count: None,
 494            max_token_count: tiktoken_rs::model::get_context_size(model),
 495            pending_token_count: Task::ready(None),
 496            model: model.into(),
 497            _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
 498            api_key,
 499            buffer,
 500        };
 501        let message = Message {
 502            id: MessageId(post_inc(&mut this.next_message_id.0)),
 503            start: language::Anchor::MIN,
 504        };
 505        this.messages.push(message.clone());
 506        this.messages_metadata.insert(
 507            message.id,
 508            MessageMetadata {
 509                role: Role::User,
 510                sent_at: Local::now(),
 511                error: None,
 512            },
 513        );
 514
 515        this.count_remaining_tokens(cx);
 516        this
 517    }
 518
 519    fn handle_buffer_event(
 520        &mut self,
 521        _: ModelHandle<Buffer>,
 522        event: &language::Event,
 523        cx: &mut ModelContext<Self>,
 524    ) {
 525        match event {
 526            language::Event::Edited => {
 527                self.count_remaining_tokens(cx);
 528                cx.emit(AssistantEvent::MessagesEdited);
 529            }
 530            _ => {}
 531        }
 532    }
 533
 534    fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
 535        let messages = self
 536            .open_ai_request_messages(cx)
 537            .into_iter()
 538            .filter_map(|message| {
 539                Some(tiktoken_rs::ChatCompletionRequestMessage {
 540                    role: match message.role {
 541                        Role::User => "user".into(),
 542                        Role::Assistant => "assistant".into(),
 543                        Role::System => "system".into(),
 544                    },
 545                    content: message.content,
 546                    name: None,
 547                })
 548            })
 549            .collect::<Vec<_>>();
 550        let model = self.model.clone();
 551        self.pending_token_count = cx.spawn_weak(|this, mut cx| {
 552            async move {
 553                cx.background().timer(Duration::from_millis(200)).await;
 554                let token_count = cx
 555                    .background()
 556                    .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) })
 557                    .await?;
 558
 559                this.upgrade(&cx)
 560                    .ok_or_else(|| anyhow!("assistant was dropped"))?
 561                    .update(&mut cx, |this, cx| {
 562                        this.max_token_count = tiktoken_rs::model::get_context_size(&this.model);
 563                        this.token_count = Some(token_count);
 564                        cx.notify()
 565                    });
 566                anyhow::Ok(())
 567            }
 568            .log_err()
 569        });
 570    }
 571
 572    fn remaining_tokens(&self) -> Option<isize> {
 573        Some(self.max_token_count as isize - self.token_count? as isize)
 574    }
 575
 576    fn set_model(&mut self, model: String, cx: &mut ModelContext<Self>) {
 577        self.model = model;
 578        self.count_remaining_tokens(cx);
 579        cx.notify();
 580    }
 581
 582    fn assist(&mut self, cx: &mut ModelContext<Self>) -> Option<(Message, Message)> {
 583        let request = OpenAIRequest {
 584            model: self.model.clone(),
 585            messages: self.open_ai_request_messages(cx),
 586            stream: true,
 587        };
 588
 589        let api_key = self.api_key.borrow().clone()?;
 590        let stream = stream_completion(api_key, cx.background().clone(), request);
 591        let assistant_message =
 592            self.insert_message_after(self.messages.last()?.id, Role::Assistant, cx)?;
 593        let user_message = self.insert_message_after(assistant_message.id, Role::User, cx)?;
 594        let task = cx.spawn_weak({
 595            |this, mut cx| async move {
 596                let assistant_message_id = assistant_message.id;
 597                let stream_completion = async {
 598                    let mut messages = stream.await?;
 599
 600                    while let Some(message) = messages.next().await {
 601                        let mut message = message?;
 602                        if let Some(choice) = message.choices.pop() {
 603                            this.upgrade(&cx)
 604                                .ok_or_else(|| anyhow!("assistant was dropped"))?
 605                                .update(&mut cx, |this, cx| {
 606                                    let text: Arc<str> = choice.delta.content?.into();
 607                                    let message_ix = this
 608                                        .messages
 609                                        .iter()
 610                                        .position(|message| message.id == assistant_message_id)?;
 611                                    this.buffer.update(cx, |buffer, cx| {
 612                                        let offset = if message_ix + 1 == this.messages.len() {
 613                                            buffer.len()
 614                                        } else {
 615                                            this.messages[message_ix + 1]
 616                                                .start
 617                                                .to_offset(buffer)
 618                                                .saturating_sub(1)
 619                                        };
 620                                        buffer.edit([(offset..offset, text)], None, cx);
 621                                    });
 622                                    cx.emit(AssistantEvent::StreamedCompletion);
 623
 624                                    Some(())
 625                                });
 626                        }
 627                    }
 628
 629                    this.upgrade(&cx)
 630                        .ok_or_else(|| anyhow!("assistant was dropped"))?
 631                        .update(&mut cx, |this, cx| {
 632                            this.pending_completions
 633                                .retain(|completion| completion.id != this.completion_count);
 634                            this.summarize(cx);
 635                        });
 636
 637                    anyhow::Ok(())
 638                };
 639
 640                let result = stream_completion.await;
 641                if let Some(this) = this.upgrade(&cx) {
 642                    this.update(&mut cx, |this, cx| {
 643                        if let Err(error) = result {
 644                            if let Some(metadata) =
 645                                this.messages_metadata.get_mut(&assistant_message.id)
 646                            {
 647                                metadata.error = Some(error.to_string().trim().into());
 648                                cx.notify();
 649                            }
 650                        }
 651                    });
 652                }
 653            }
 654        });
 655
 656        self.pending_completions.push(PendingCompletion {
 657            id: post_inc(&mut self.completion_count),
 658            _task: task,
 659        });
 660        Some((assistant_message, user_message))
 661    }
 662
 663    fn cancel_last_assist(&mut self) -> bool {
 664        self.pending_completions.pop().is_some()
 665    }
 666
 667    fn cycle_message_role(&mut self, id: MessageId, cx: &mut ModelContext<Self>) {
 668        if let Some(metadata) = self.messages_metadata.get_mut(&id) {
 669            metadata.role.cycle();
 670            cx.emit(AssistantEvent::MessagesEdited);
 671            cx.notify();
 672        }
 673    }
 674
 675    fn insert_message_after(
 676        &mut self,
 677        message_id: MessageId,
 678        role: Role,
 679        cx: &mut ModelContext<Self>,
 680    ) -> Option<Message> {
 681        if let Some(prev_message_ix) = self
 682            .messages
 683            .iter()
 684            .position(|message| message.id == message_id)
 685        {
 686            let start = self.buffer.update(cx, |buffer, cx| {
 687                let offset = self.messages[prev_message_ix + 1..]
 688                    .iter()
 689                    .find(|message| message.start.is_valid(buffer))
 690                    .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
 691                buffer.edit([(offset..offset, "\n")], None, cx);
 692                buffer.anchor_before(offset + 1)
 693            });
 694            let message = Message {
 695                id: MessageId(post_inc(&mut self.next_message_id.0)),
 696                start,
 697            };
 698            self.messages.insert(prev_message_ix + 1, message.clone());
 699            self.messages_metadata.insert(
 700                message.id,
 701                MessageMetadata {
 702                    role,
 703                    sent_at: Local::now(),
 704                    error: None,
 705                },
 706            );
 707            cx.emit(AssistantEvent::MessagesEdited);
 708            Some(message)
 709        } else {
 710            None
 711        }
 712    }
 713
 714    fn summarize(&mut self, cx: &mut ModelContext<Self>) {
 715        if self.messages.len() >= 2 && self.summary.is_none() {
 716            let api_key = self.api_key.borrow().clone();
 717            if let Some(api_key) = api_key {
 718                let mut messages = self.open_ai_request_messages(cx);
 719                messages.truncate(2);
 720                messages.push(RequestMessage {
 721                    role: Role::User,
 722                    content: "Summarize the conversation into a short title without punctuation"
 723                        .into(),
 724                });
 725                let request = OpenAIRequest {
 726                    model: self.model.clone(),
 727                    messages,
 728                    stream: true,
 729                };
 730
 731                let stream = stream_completion(api_key, cx.background().clone(), request);
 732                self.pending_summary = cx.spawn(|this, mut cx| {
 733                    async move {
 734                        let mut messages = stream.await?;
 735
 736                        while let Some(message) = messages.next().await {
 737                            let mut message = message?;
 738                            if let Some(choice) = message.choices.pop() {
 739                                let text = choice.delta.content.unwrap_or_default();
 740                                this.update(&mut cx, |this, cx| {
 741                                    this.summary.get_or_insert(String::new()).push_str(&text);
 742                                    cx.emit(AssistantEvent::SummaryChanged);
 743                                });
 744                            }
 745                        }
 746
 747                        anyhow::Ok(())
 748                    }
 749                    .log_err()
 750                });
 751            }
 752        }
 753    }
 754
 755    fn open_ai_request_messages(&self, cx: &AppContext) -> Vec<RequestMessage> {
 756        let buffer = self.buffer.read(cx);
 757        self.messages(cx)
 758            .map(|(_message, metadata, range)| RequestMessage {
 759                role: metadata.role,
 760                content: buffer.text_for_range(range).collect(),
 761            })
 762            .collect()
 763    }
 764
 765    fn message_id_for_offset(&self, offset: usize, cx: &AppContext) -> Option<MessageId> {
 766        Some(
 767            self.messages(cx)
 768                .find(|(_, _, range)| range.contains(&offset))
 769                .map(|(message, _, _)| message)
 770                .or(self.messages.last())?
 771                .id,
 772        )
 773    }
 774
 775    fn messages<'a>(
 776        &'a self,
 777        cx: &'a AppContext,
 778    ) -> impl 'a + Iterator<Item = (&Message, &MessageMetadata, Range<usize>)> {
 779        let buffer = self.buffer.read(cx);
 780        let mut messages = self.messages.iter().peekable();
 781        iter::from_fn(move || {
 782            while let Some(message) = messages.next() {
 783                let metadata = self.messages_metadata.get(&message.id)?;
 784                let message_start = message.start.to_offset(buffer);
 785                let mut message_end = None;
 786                while let Some(next_message) = messages.peek() {
 787                    if next_message.start.is_valid(buffer) {
 788                        message_end = Some(next_message.start);
 789                        break;
 790                    } else {
 791                        messages.next();
 792                    }
 793                }
 794                let message_end = message_end
 795                    .unwrap_or(language::Anchor::MAX)
 796                    .to_offset(buffer);
 797                return Some((message, metadata, message_start..message_end));
 798            }
 799            None
 800        })
 801    }
 802}
 803
 804struct PendingCompletion {
 805    id: usize,
 806    _task: Task<()>,
 807}
 808
 809enum AssistantEditorEvent {
 810    TabContentChanged,
 811}
 812
 813#[derive(Copy, Clone, Debug, PartialEq)]
 814struct ScrollPosition {
 815    offset_before_cursor: Vector2F,
 816    cursor: Anchor,
 817}
 818
 819struct AssistantEditor {
 820    assistant: ModelHandle<Assistant>,
 821    editor: ViewHandle<Editor>,
 822    blocks: HashSet<BlockId>,
 823    scroll_position: Option<ScrollPosition>,
 824    _subscriptions: Vec<Subscription>,
 825}
 826
 827impl AssistantEditor {
 828    fn new(
 829        api_key: Rc<RefCell<Option<String>>>,
 830        language_registry: Arc<LanguageRegistry>,
 831        cx: &mut ViewContext<Self>,
 832    ) -> Self {
 833        let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx));
 834        let editor = cx.add_view(|cx| {
 835            let mut editor = Editor::for_buffer(assistant.read(cx).buffer.clone(), None, cx);
 836            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 837            editor.set_show_gutter(false, cx);
 838            editor
 839        });
 840
 841        let _subscriptions = vec![
 842            cx.observe(&assistant, |_, _, cx| cx.notify()),
 843            cx.subscribe(&assistant, Self::handle_assistant_event),
 844            cx.subscribe(&editor, Self::handle_editor_event),
 845        ];
 846
 847        let mut this = Self {
 848            assistant,
 849            editor,
 850            blocks: Default::default(),
 851            scroll_position: None,
 852            _subscriptions,
 853        };
 854        this.update_message_headers(cx);
 855        this
 856    }
 857
 858    fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
 859        let user_message = self.assistant.update(cx, |assistant, cx| {
 860            let editor = self.editor.read(cx);
 861            let newest_selection = editor
 862                .selections
 863                .newest_anchor()
 864                .head()
 865                .to_offset(&editor.buffer().read(cx).snapshot(cx));
 866            let message_id = assistant.message_id_for_offset(newest_selection, cx)?;
 867            let metadata = assistant.messages_metadata.get(&message_id)?;
 868            let user_message = if metadata.role == Role::User {
 869                let (_, user_message) = assistant.assist(cx)?;
 870                user_message
 871            } else {
 872                let user_message = assistant.insert_message_after(message_id, Role::User, cx)?;
 873                user_message
 874            };
 875            Some(user_message)
 876        });
 877
 878        if let Some(user_message) = user_message {
 879            let cursor = user_message
 880                .start
 881                .to_offset(&self.assistant.read(cx).buffer.read(cx));
 882            self.editor.update(cx, |editor, cx| {
 883                editor.change_selections(
 884                    Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
 885                    cx,
 886                    |selections| selections.select_ranges([cursor..cursor]),
 887                );
 888            });
 889        }
 890    }
 891
 892    fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
 893        if !self
 894            .assistant
 895            .update(cx, |assistant, _| assistant.cancel_last_assist())
 896        {
 897            cx.propagate_action();
 898        }
 899    }
 900
 901    fn handle_assistant_event(
 902        &mut self,
 903        _: ModelHandle<Assistant>,
 904        event: &AssistantEvent,
 905        cx: &mut ViewContext<Self>,
 906    ) {
 907        match event {
 908            AssistantEvent::MessagesEdited => self.update_message_headers(cx),
 909            AssistantEvent::SummaryChanged => {
 910                cx.emit(AssistantEditorEvent::TabContentChanged);
 911            }
 912            AssistantEvent::StreamedCompletion => {
 913                self.editor.update(cx, |editor, cx| {
 914                    if let Some(scroll_position) = self.scroll_position {
 915                        let snapshot = editor.snapshot(cx);
 916                        let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
 917                        let scroll_top =
 918                            cursor_point.row() as f32 - scroll_position.offset_before_cursor.y();
 919                        editor.set_scroll_position(
 920                            vec2f(scroll_position.offset_before_cursor.x(), scroll_top),
 921                            cx,
 922                        );
 923                    }
 924                });
 925            }
 926        }
 927    }
 928
 929    fn handle_editor_event(
 930        &mut self,
 931        _: ViewHandle<Editor>,
 932        event: &editor::Event,
 933        cx: &mut ViewContext<Self>,
 934    ) {
 935        match event {
 936            editor::Event::ScrollPositionChanged { autoscroll, .. } => {
 937                let cursor_scroll_position = self.cursor_scroll_position(cx);
 938                if *autoscroll {
 939                    self.scroll_position = cursor_scroll_position;
 940                } else if self.scroll_position != cursor_scroll_position {
 941                    self.scroll_position = None;
 942                }
 943            }
 944            editor::Event::SelectionsChanged { .. } => {
 945                self.scroll_position = self.cursor_scroll_position(cx);
 946            }
 947            _ => {}
 948        }
 949    }
 950
 951    fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
 952        self.editor.update(cx, |editor, cx| {
 953            let snapshot = editor.snapshot(cx);
 954            let cursor = editor.selections.newest_anchor().head();
 955            let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32;
 956            let scroll_position = editor
 957                .scroll_manager
 958                .anchor()
 959                .scroll_position(&snapshot.display_snapshot);
 960
 961            let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.);
 962            if (scroll_position.y()..scroll_bottom).contains(&cursor_row) {
 963                Some(ScrollPosition {
 964                    cursor,
 965                    offset_before_cursor: vec2f(
 966                        scroll_position.x(),
 967                        cursor_row - scroll_position.y(),
 968                    ),
 969                })
 970            } else {
 971                None
 972            }
 973        })
 974    }
 975
 976    fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
 977        self.editor.update(cx, |editor, cx| {
 978            let buffer = editor.buffer().read(cx).snapshot(cx);
 979            let excerpt_id = *buffer.as_singleton().unwrap().0;
 980            let old_blocks = std::mem::take(&mut self.blocks);
 981            let new_blocks = self
 982                .assistant
 983                .read(cx)
 984                .messages(cx)
 985                .map(|(message, metadata, _)| BlockProperties {
 986                    position: buffer.anchor_in_excerpt(excerpt_id, message.start),
 987                    height: 2,
 988                    style: BlockStyle::Sticky,
 989                    render: Arc::new({
 990                        let assistant = self.assistant.clone();
 991                        let metadata = metadata.clone();
 992                        let message = message.clone();
 993                        move |cx| {
 994                            enum Sender {}
 995                            enum ErrorTooltip {}
 996
 997                            let theme = theme::current(cx);
 998                            let style = &theme.assistant;
 999                            let message_id = message.id;
1000                            let sender = MouseEventHandler::<Sender, _>::new(
1001                                message_id.0,
1002                                cx,
1003                                |state, _| match metadata.role {
1004                                    Role::User => {
1005                                        let style = style.user_sender.style_for(state, false);
1006                                        Label::new("You", style.text.clone())
1007                                            .contained()
1008                                            .with_style(style.container)
1009                                    }
1010                                    Role::Assistant => {
1011                                        let style = style.assistant_sender.style_for(state, false);
1012                                        Label::new("Assistant", style.text.clone())
1013                                            .contained()
1014                                            .with_style(style.container)
1015                                    }
1016                                    Role::System => {
1017                                        let style = style.system_sender.style_for(state, false);
1018                                        Label::new("System", style.text.clone())
1019                                            .contained()
1020                                            .with_style(style.container)
1021                                    }
1022                                },
1023                            )
1024                            .with_cursor_style(CursorStyle::PointingHand)
1025                            .on_down(MouseButton::Left, {
1026                                let assistant = assistant.clone();
1027                                move |_, _, cx| {
1028                                    assistant.update(cx, |assistant, cx| {
1029                                        assistant.cycle_message_role(message_id, cx)
1030                                    })
1031                                }
1032                            });
1033
1034                            Flex::row()
1035                                .with_child(sender.aligned())
1036                                .with_child(
1037                                    Label::new(
1038                                        metadata.sent_at.format("%I:%M%P").to_string(),
1039                                        style.sent_at.text.clone(),
1040                                    )
1041                                    .contained()
1042                                    .with_style(style.sent_at.container)
1043                                    .aligned(),
1044                                )
1045                                .with_children(metadata.error.clone().map(|error| {
1046                                    Svg::new("icons/circle_x_mark_12.svg")
1047                                        .with_color(style.error_icon.color)
1048                                        .constrained()
1049                                        .with_width(style.error_icon.width)
1050                                        .contained()
1051                                        .with_style(style.error_icon.container)
1052                                        .with_tooltip::<ErrorTooltip>(
1053                                            message_id.0,
1054                                            error,
1055                                            None,
1056                                            theme.tooltip.clone(),
1057                                            cx,
1058                                        )
1059                                        .aligned()
1060                                }))
1061                                .aligned()
1062                                .left()
1063                                .contained()
1064                                .with_style(style.header)
1065                                .into_any()
1066                        }
1067                    }),
1068                    disposition: BlockDisposition::Above,
1069                })
1070                .collect::<Vec<_>>();
1071
1072            editor.remove_blocks(old_blocks, None, cx);
1073            let ids = editor.insert_blocks(new_blocks, None, cx);
1074            self.blocks = HashSet::from_iter(ids);
1075        });
1076    }
1077
1078    fn quote_selection(
1079        workspace: &mut Workspace,
1080        _: &QuoteSelection,
1081        cx: &mut ViewContext<Workspace>,
1082    ) {
1083        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1084            return;
1085        };
1086        let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else {
1087            return;
1088        };
1089
1090        let text = editor.read_with(cx, |editor, cx| {
1091            let range = editor.selections.newest::<usize>(cx).range();
1092            let buffer = editor.buffer().read(cx).snapshot(cx);
1093            let start_language = buffer.language_at(range.start);
1094            let end_language = buffer.language_at(range.end);
1095            let language_name = if start_language == end_language {
1096                start_language.map(|language| language.name())
1097            } else {
1098                None
1099            };
1100            let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
1101
1102            let selected_text = buffer.text_for_range(range).collect::<String>();
1103            if selected_text.is_empty() {
1104                None
1105            } else {
1106                Some(if language_name == "markdown" {
1107                    selected_text
1108                        .lines()
1109                        .map(|line| format!("> {}", line))
1110                        .collect::<Vec<_>>()
1111                        .join("\n")
1112                } else {
1113                    format!("```{language_name}\n{selected_text}\n```")
1114                })
1115            }
1116        });
1117
1118        // Activate the panel
1119        if !panel.read(cx).has_focus(cx) {
1120            workspace.toggle_panel_focus::<AssistantPanel>(cx);
1121        }
1122
1123        if let Some(text) = text {
1124            panel.update(cx, |panel, cx| {
1125                if let Some(assistant) = panel
1126                    .pane
1127                    .read(cx)
1128                    .active_item()
1129                    .and_then(|item| item.downcast::<AssistantEditor>())
1130                    .ok_or_else(|| anyhow!("no active context"))
1131                    .log_err()
1132                {
1133                    assistant.update(cx, |assistant, cx| {
1134                        assistant
1135                            .editor
1136                            .update(cx, |editor, cx| editor.insert(&text, cx))
1137                    });
1138                }
1139            });
1140        }
1141    }
1142
1143    fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext<Self>) {
1144        let editor = self.editor.read(cx);
1145        let assistant = self.assistant.read(cx);
1146        if editor.selections.count() == 1 {
1147            let selection = editor.selections.newest::<usize>(cx);
1148            let mut copied_text = String::new();
1149            let mut spanned_messages = 0;
1150            for (_message, metadata, message_range) in assistant.messages(cx) {
1151                if message_range.start >= selection.range().end {
1152                    break;
1153                } else if message_range.end >= selection.range().start {
1154                    let range = cmp::max(message_range.start, selection.range().start)
1155                        ..cmp::min(message_range.end, selection.range().end);
1156                    if !range.is_empty() {
1157                        spanned_messages += 1;
1158                        write!(&mut copied_text, "## {}\n\n", metadata.role).unwrap();
1159                        for chunk in assistant.buffer.read(cx).text_for_range(range) {
1160                            copied_text.push_str(&chunk);
1161                        }
1162                        copied_text.push('\n');
1163                    }
1164                }
1165            }
1166
1167            if spanned_messages > 1 {
1168                cx.platform()
1169                    .write_to_clipboard(ClipboardItem::new(copied_text));
1170                return;
1171            }
1172        }
1173
1174        cx.propagate_action();
1175    }
1176
1177    fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
1178        self.assistant.update(cx, |assistant, cx| {
1179            let new_model = match assistant.model.as_str() {
1180                "gpt-4" => "gpt-3.5-turbo",
1181                _ => "gpt-4",
1182            };
1183            assistant.set_model(new_model.into(), cx);
1184        });
1185    }
1186
1187    fn title(&self, cx: &AppContext) -> String {
1188        self.assistant
1189            .read(cx)
1190            .summary
1191            .clone()
1192            .unwrap_or_else(|| "New Context".into())
1193    }
1194}
1195
1196impl Entity for AssistantEditor {
1197    type Event = AssistantEditorEvent;
1198}
1199
1200impl View for AssistantEditor {
1201    fn ui_name() -> &'static str {
1202        "AssistantEditor"
1203    }
1204
1205    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1206        enum Model {}
1207        let theme = &theme::current(cx).assistant;
1208        let assistant = &self.assistant.read(cx);
1209        let model = assistant.model.clone();
1210        let remaining_tokens = assistant.remaining_tokens().map(|remaining_tokens| {
1211            let remaining_tokens_style = if remaining_tokens <= 0 {
1212                &theme.no_remaining_tokens
1213            } else {
1214                &theme.remaining_tokens
1215            };
1216            Label::new(
1217                remaining_tokens.to_string(),
1218                remaining_tokens_style.text.clone(),
1219            )
1220            .contained()
1221            .with_style(remaining_tokens_style.container)
1222        });
1223
1224        Stack::new()
1225            .with_child(
1226                ChildView::new(&self.editor, cx)
1227                    .contained()
1228                    .with_style(theme.container),
1229            )
1230            .with_child(
1231                Flex::row()
1232                    .with_child(
1233                        MouseEventHandler::<Model, _>::new(0, cx, |state, _| {
1234                            let style = theme.model.style_for(state, false);
1235                            Label::new(model, style.text.clone())
1236                                .contained()
1237                                .with_style(style.container)
1238                        })
1239                        .with_cursor_style(CursorStyle::PointingHand)
1240                        .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)),
1241                    )
1242                    .with_children(remaining_tokens)
1243                    .contained()
1244                    .with_style(theme.model_info_container)
1245                    .aligned()
1246                    .top()
1247                    .right(),
1248            )
1249            .into_any()
1250    }
1251
1252    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1253        if cx.is_self_focused() {
1254            cx.focus(&self.editor);
1255        }
1256    }
1257}
1258
1259impl Item for AssistantEditor {
1260    fn tab_content<V: View>(
1261        &self,
1262        _: Option<usize>,
1263        style: &theme::Tab,
1264        cx: &gpui::AppContext,
1265    ) -> AnyElement<V> {
1266        let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN);
1267        Label::new(title, style.label.clone()).into_any()
1268    }
1269
1270    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
1271        Some(self.title(cx).into())
1272    }
1273
1274    fn as_searchable(
1275        &self,
1276        _: &ViewHandle<Self>,
1277    ) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
1278        Some(Box::new(self.editor.clone()))
1279    }
1280}
1281
1282#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
1283struct MessageId(usize);
1284
1285#[derive(Clone, Debug)]
1286struct Message {
1287    id: MessageId,
1288    start: language::Anchor,
1289}
1290
1291#[derive(Clone, Debug)]
1292struct MessageMetadata {
1293    role: Role,
1294    sent_at: DateTime<Local>,
1295    error: Option<String>,
1296}
1297
1298async fn stream_completion(
1299    api_key: String,
1300    executor: Arc<Background>,
1301    mut request: OpenAIRequest,
1302) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
1303    request.stream = true;
1304
1305    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
1306
1307    let json_data = serde_json::to_string(&request)?;
1308    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
1309        .header("Content-Type", "application/json")
1310        .header("Authorization", format!("Bearer {}", api_key))
1311        .body(json_data)?
1312        .send_async()
1313        .await?;
1314
1315    let status = response.status();
1316    if status == StatusCode::OK {
1317        executor
1318            .spawn(async move {
1319                let mut lines = BufReader::new(response.body_mut()).lines();
1320
1321                fn parse_line(
1322                    line: Result<String, io::Error>,
1323                ) -> Result<Option<OpenAIResponseStreamEvent>> {
1324                    if let Some(data) = line?.strip_prefix("data: ") {
1325                        let event = serde_json::from_str(&data)?;
1326                        Ok(Some(event))
1327                    } else {
1328                        Ok(None)
1329                    }
1330                }
1331
1332                while let Some(line) = lines.next().await {
1333                    if let Some(event) = parse_line(line).transpose() {
1334                        let done = event.as_ref().map_or(false, |event| {
1335                            event
1336                                .choices
1337                                .last()
1338                                .map_or(false, |choice| choice.finish_reason.is_some())
1339                        });
1340                        if tx.unbounded_send(event).is_err() {
1341                            break;
1342                        }
1343
1344                        if done {
1345                            break;
1346                        }
1347                    }
1348                }
1349
1350                anyhow::Ok(())
1351            })
1352            .detach();
1353
1354        Ok(rx)
1355    } else {
1356        let mut body = String::new();
1357        response.body_mut().read_to_string(&mut body).await?;
1358
1359        #[derive(Deserialize)]
1360        struct OpenAIResponse {
1361            error: OpenAIError,
1362        }
1363
1364        #[derive(Deserialize)]
1365        struct OpenAIError {
1366            message: String,
1367        }
1368
1369        match serde_json::from_str::<OpenAIResponse>(&body) {
1370            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
1371                "Failed to connect to OpenAI API: {}",
1372                response.error.message,
1373            )),
1374
1375            _ => Err(anyhow!(
1376                "Failed to connect to OpenAI API: {} {}",
1377                response.status(),
1378                body,
1379            )),
1380        }
1381    }
1382}
1383
1384#[cfg(test)]
1385mod tests {
1386    use super::*;
1387    use gpui::AppContext;
1388
1389    #[gpui::test]
1390    fn test_inserting_and_removing_messages(cx: &mut AppContext) {
1391        let registry = Arc::new(LanguageRegistry::test());
1392        let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx));
1393        let buffer = assistant.read(cx).buffer.clone();
1394
1395        let message_1 = assistant.read(cx).messages[0].clone();
1396        assert_eq!(
1397            messages(&assistant, cx),
1398            vec![(message_1.id, Role::User, 0..0)]
1399        );
1400
1401        let message_2 = assistant.update(cx, |assistant, cx| {
1402            assistant
1403                .insert_message_after(message_1.id, Role::Assistant, cx)
1404                .unwrap()
1405        });
1406        assert_eq!(
1407            messages(&assistant, cx),
1408            vec![
1409                (message_1.id, Role::User, 0..1),
1410                (message_2.id, Role::Assistant, 1..1)
1411            ]
1412        );
1413
1414        buffer.update(cx, |buffer, cx| {
1415            buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
1416        });
1417        assert_eq!(
1418            messages(&assistant, cx),
1419            vec![
1420                (message_1.id, Role::User, 0..2),
1421                (message_2.id, Role::Assistant, 2..3)
1422            ]
1423        );
1424
1425        let message_3 = assistant.update(cx, |assistant, cx| {
1426            assistant
1427                .insert_message_after(message_2.id, Role::User, cx)
1428                .unwrap()
1429        });
1430        assert_eq!(
1431            messages(&assistant, cx),
1432            vec![
1433                (message_1.id, Role::User, 0..2),
1434                (message_2.id, Role::Assistant, 2..4),
1435                (message_3.id, Role::User, 4..4)
1436            ]
1437        );
1438
1439        let message_4 = assistant.update(cx, |assistant, cx| {
1440            assistant
1441                .insert_message_after(message_2.id, Role::User, cx)
1442                .unwrap()
1443        });
1444        assert_eq!(
1445            messages(&assistant, cx),
1446            vec![
1447                (message_1.id, Role::User, 0..2),
1448                (message_2.id, Role::Assistant, 2..4),
1449                (message_4.id, Role::User, 4..5),
1450                (message_3.id, Role::User, 5..5),
1451            ]
1452        );
1453
1454        buffer.update(cx, |buffer, cx| {
1455            buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
1456        });
1457        assert_eq!(
1458            messages(&assistant, cx),
1459            vec![
1460                (message_1.id, Role::User, 0..2),
1461                (message_2.id, Role::Assistant, 2..4),
1462                (message_4.id, Role::User, 4..6),
1463                (message_3.id, Role::User, 6..7),
1464            ]
1465        );
1466
1467        // Deleting across message boundaries merges the messages.
1468        buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
1469        assert_eq!(
1470            messages(&assistant, cx),
1471            vec![
1472                (message_1.id, Role::User, 0..3),
1473                (message_3.id, Role::User, 3..4),
1474            ]
1475        );
1476
1477        // Undoing the deletion should also undo the merge.
1478        buffer.update(cx, |buffer, cx| buffer.undo(cx));
1479        assert_eq!(
1480            messages(&assistant, cx),
1481            vec![
1482                (message_1.id, Role::User, 0..2),
1483                (message_2.id, Role::Assistant, 2..4),
1484                (message_4.id, Role::User, 4..6),
1485                (message_3.id, Role::User, 6..7),
1486            ]
1487        );
1488
1489        // Redoing the deletion should also redo the merge.
1490        buffer.update(cx, |buffer, cx| buffer.redo(cx));
1491        assert_eq!(
1492            messages(&assistant, cx),
1493            vec![
1494                (message_1.id, Role::User, 0..3),
1495                (message_3.id, Role::User, 3..4),
1496            ]
1497        );
1498
1499        // Ensure we can still insert after a merged message.
1500        let message_5 = assistant.update(cx, |assistant, cx| {
1501            assistant
1502                .insert_message_after(message_1.id, Role::System, cx)
1503                .unwrap()
1504        });
1505        assert_eq!(
1506            messages(&assistant, cx),
1507            vec![
1508                (message_1.id, Role::User, 0..3),
1509                (message_5.id, Role::System, 3..4),
1510                (message_3.id, Role::User, 4..5)
1511            ]
1512        );
1513    }
1514
1515    fn messages(
1516        assistant: &ModelHandle<Assistant>,
1517        cx: &AppContext,
1518    ) -> Vec<(MessageId, Role, Range<usize>)> {
1519        assistant
1520            .read(cx)
1521            .messages(cx)
1522            .map(|(message, metadata, range)| (message.id, metadata.role, range))
1523            .collect()
1524    }
1525}