assistant.rs

   1use crate::{
   2    assistant_settings::{AssistantDockPosition, AssistantSettings},
   3    OpenAIRequest, OpenAIResponseStreamEvent, RequestMessage, Role, SavedConversation,
   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,
  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, env, fmt::Write, io, iter, ops::Range, path::PathBuf, rc::Rc,
  30    sync::Arc, time::Duration,
  31};
  32use util::{
  33    channel::ReleaseChannel, paths::CONVERSATIONS_DIR, post_inc, truncate_and_trailoff, ResultExt,
  34    TryFutureExt,
  35};
  36use workspace::{
  37    dock::{DockPosition, Panel},
  38    item::Item,
  39    pane, Pane, Save, Workspace,
  40};
  41
  42const OPENAI_API_URL: &'static str = "https://api.openai.com/v1";
  43
  44actions!(
  45    assistant,
  46    [
  47        NewContext,
  48        Assist,
  49        Split,
  50        CycleMessageRole,
  51        QuoteSelection,
  52        ToggleFocus,
  53        ResetKey,
  54    ]
  55);
  56
  57pub fn init(cx: &mut AppContext) {
  58    if *util::channel::RELEASE_CHANNEL == ReleaseChannel::Stable {
  59        cx.update_default_global::<collections::CommandPaletteFilter, _, _>(move |filter, _cx| {
  60            filter.filtered_namespaces.insert("assistant");
  61        });
  62    }
  63
  64    settings::register::<AssistantSettings>(cx);
  65    cx.add_action(
  66        |workspace: &mut Workspace, _: &NewContext, cx: &mut ViewContext<Workspace>| {
  67            if let Some(this) = workspace.panel::<AssistantPanel>(cx) {
  68                this.update(cx, |this, cx| this.add_context(cx))
  69            }
  70
  71            workspace.focus_panel::<AssistantPanel>(cx);
  72        },
  73    );
  74    cx.add_action(AssistantEditor::assist);
  75    cx.capture_action(AssistantEditor::cancel_last_assist);
  76    cx.capture_action(AssistantEditor::save);
  77    cx.add_action(AssistantEditor::quote_selection);
  78    cx.capture_action(AssistantEditor::copy);
  79    cx.capture_action(AssistantEditor::split);
  80    cx.capture_action(AssistantEditor::cycle_message_role);
  81    cx.add_action(AssistantPanel::save_api_key);
  82    cx.add_action(AssistantPanel::reset_api_key);
  83    cx.add_action(
  84        |workspace: &mut Workspace, _: &ToggleFocus, cx: &mut ViewContext<Workspace>| {
  85            workspace.toggle_panel_focus::<AssistantPanel>(cx);
  86        },
  87    );
  88}
  89
  90pub enum AssistantPanelEvent {
  91    ZoomIn,
  92    ZoomOut,
  93    Focus,
  94    Close,
  95    DockPositionChanged,
  96}
  97
  98pub struct AssistantPanel {
  99    width: Option<f32>,
 100    height: Option<f32>,
 101    pane: ViewHandle<Pane>,
 102    api_key: Rc<RefCell<Option<String>>>,
 103    api_key_editor: Option<ViewHandle<Editor>>,
 104    has_read_credentials: bool,
 105    languages: Arc<LanguageRegistry>,
 106    fs: Arc<dyn Fs>,
 107    subscriptions: Vec<Subscription>,
 108}
 109
 110impl AssistantPanel {
 111    pub fn load(
 112        workspace: WeakViewHandle<Workspace>,
 113        cx: AsyncAppContext,
 114    ) -> Task<Result<ViewHandle<Self>>> {
 115        cx.spawn(|mut cx| async move {
 116            // TODO: deserialize state.
 117            workspace.update(&mut cx, |workspace, cx| {
 118                cx.add_view::<Self, _>(|cx| {
 119                    let weak_self = cx.weak_handle();
 120                    let pane = cx.add_view(|cx| {
 121                        let mut pane = Pane::new(
 122                            workspace.weak_handle(),
 123                            workspace.project().clone(),
 124                            workspace.app_state().background_actions,
 125                            Default::default(),
 126                            cx,
 127                        );
 128                        pane.set_can_split(false, cx);
 129                        pane.set_can_navigate(false, cx);
 130                        pane.on_can_drop(move |_, _| false);
 131                        pane.set_render_tab_bar_buttons(cx, move |pane, cx| {
 132                            let weak_self = weak_self.clone();
 133                            Flex::row()
 134                                .with_child(Pane::render_tab_bar_button(
 135                                    0,
 136                                    "icons/plus_12.svg",
 137                                    false,
 138                                    Some(("New Context".into(), Some(Box::new(NewContext)))),
 139                                    cx,
 140                                    move |_, cx| {
 141                                        let weak_self = weak_self.clone();
 142                                        cx.window_context().defer(move |cx| {
 143                                            if let Some(this) = weak_self.upgrade(cx) {
 144                                                this.update(cx, |this, cx| this.add_context(cx));
 145                                            }
 146                                        })
 147                                    },
 148                                    None,
 149                                ))
 150                                .with_child(Pane::render_tab_bar_button(
 151                                    1,
 152                                    if pane.is_zoomed() {
 153                                        "icons/minimize_8.svg"
 154                                    } else {
 155                                        "icons/maximize_8.svg"
 156                                    },
 157                                    pane.is_zoomed(),
 158                                    Some((
 159                                        "Toggle Zoom".into(),
 160                                        Some(Box::new(workspace::ToggleZoom)),
 161                                    )),
 162                                    cx,
 163                                    move |pane, cx| pane.toggle_zoom(&Default::default(), cx),
 164                                    None,
 165                                ))
 166                                .into_any()
 167                        });
 168                        let buffer_search_bar = cx.add_view(search::BufferSearchBar::new);
 169                        pane.toolbar()
 170                            .update(cx, |toolbar, cx| toolbar.add_item(buffer_search_bar, cx));
 171                        pane
 172                    });
 173
 174                    let mut this = Self {
 175                        pane,
 176                        api_key: Rc::new(RefCell::new(None)),
 177                        api_key_editor: None,
 178                        has_read_credentials: false,
 179                        languages: workspace.app_state().languages.clone(),
 180                        fs: workspace.app_state().fs.clone(),
 181                        width: None,
 182                        height: None,
 183                        subscriptions: Default::default(),
 184                    };
 185
 186                    let mut old_dock_position = this.position(cx);
 187                    this.subscriptions = vec![
 188                        cx.observe(&this.pane, |_, _, cx| cx.notify()),
 189                        cx.subscribe(&this.pane, Self::handle_pane_event),
 190                        cx.observe_global::<SettingsStore, _>(move |this, cx| {
 191                            let new_dock_position = this.position(cx);
 192                            if new_dock_position != old_dock_position {
 193                                old_dock_position = new_dock_position;
 194                                cx.emit(AssistantPanelEvent::DockPositionChanged);
 195                            }
 196                        }),
 197                    ];
 198
 199                    this
 200                })
 201            })
 202        })
 203    }
 204
 205    fn handle_pane_event(
 206        &mut self,
 207        _pane: ViewHandle<Pane>,
 208        event: &pane::Event,
 209        cx: &mut ViewContext<Self>,
 210    ) {
 211        match event {
 212            pane::Event::ZoomIn => cx.emit(AssistantPanelEvent::ZoomIn),
 213            pane::Event::ZoomOut => cx.emit(AssistantPanelEvent::ZoomOut),
 214            pane::Event::Focus => cx.emit(AssistantPanelEvent::Focus),
 215            pane::Event::Remove => cx.emit(AssistantPanelEvent::Close),
 216            _ => {}
 217        }
 218    }
 219
 220    fn add_context(&mut self, cx: &mut ViewContext<Self>) {
 221        let focus = self.has_focus(cx);
 222        let editor = cx.add_view(|cx| {
 223            AssistantEditor::new(
 224                self.api_key.clone(),
 225                self.languages.clone(),
 226                self.fs.clone(),
 227                cx,
 228            )
 229        });
 230        self.subscriptions
 231            .push(cx.subscribe(&editor, Self::handle_assistant_editor_event));
 232        self.pane.update(cx, |pane, cx| {
 233            pane.add_item(Box::new(editor), true, focus, None, cx)
 234        });
 235    }
 236
 237    fn handle_assistant_editor_event(
 238        &mut self,
 239        _: ViewHandle<AssistantEditor>,
 240        event: &AssistantEditorEvent,
 241        cx: &mut ViewContext<Self>,
 242    ) {
 243        match event {
 244            AssistantEditorEvent::TabContentChanged => self.pane.update(cx, |_, cx| cx.notify()),
 245        }
 246    }
 247
 248    fn save_api_key(&mut self, _: &menu::Confirm, cx: &mut ViewContext<Self>) {
 249        if let Some(api_key) = self
 250            .api_key_editor
 251            .as_ref()
 252            .map(|editor| editor.read(cx).text(cx))
 253        {
 254            if !api_key.is_empty() {
 255                cx.platform()
 256                    .write_credentials(OPENAI_API_URL, "Bearer", api_key.as_bytes())
 257                    .log_err();
 258                *self.api_key.borrow_mut() = Some(api_key);
 259                self.api_key_editor.take();
 260                cx.focus_self();
 261                cx.notify();
 262            }
 263        } else {
 264            cx.propagate_action();
 265        }
 266    }
 267
 268    fn reset_api_key(&mut self, _: &ResetKey, cx: &mut ViewContext<Self>) {
 269        cx.platform().delete_credentials(OPENAI_API_URL).log_err();
 270        self.api_key.take();
 271        self.api_key_editor = Some(build_api_key_editor(cx));
 272        cx.focus_self();
 273        cx.notify();
 274    }
 275}
 276
 277fn build_api_key_editor(cx: &mut ViewContext<AssistantPanel>) -> ViewHandle<Editor> {
 278    cx.add_view(|cx| {
 279        let mut editor = Editor::single_line(
 280            Some(Arc::new(|theme| theme.assistant.api_key_editor.clone())),
 281            cx,
 282        );
 283        editor.set_placeholder_text("sk-000000000000000000000000000000000000000000000000", cx);
 284        editor
 285    })
 286}
 287
 288impl Entity for AssistantPanel {
 289    type Event = AssistantPanelEvent;
 290}
 291
 292impl View for AssistantPanel {
 293    fn ui_name() -> &'static str {
 294        "AssistantPanel"
 295    }
 296
 297    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
 298        let style = &theme::current(cx).assistant;
 299        if let Some(api_key_editor) = self.api_key_editor.as_ref() {
 300            Flex::column()
 301                .with_child(
 302                    Text::new(
 303                        "Paste your OpenAI API key and press Enter to use the assistant",
 304                        style.api_key_prompt.text.clone(),
 305                    )
 306                    .aligned(),
 307                )
 308                .with_child(
 309                    ChildView::new(api_key_editor, cx)
 310                        .contained()
 311                        .with_style(style.api_key_editor.container)
 312                        .aligned(),
 313                )
 314                .contained()
 315                .with_style(style.api_key_prompt.container)
 316                .aligned()
 317                .into_any()
 318        } else {
 319            ChildView::new(&self.pane, cx).into_any()
 320        }
 321    }
 322
 323    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
 324        if cx.is_self_focused() {
 325            if let Some(api_key_editor) = self.api_key_editor.as_ref() {
 326                cx.focus(api_key_editor);
 327            } else {
 328                cx.focus(&self.pane);
 329            }
 330        }
 331    }
 332}
 333
 334impl Panel for AssistantPanel {
 335    fn position(&self, cx: &WindowContext) -> DockPosition {
 336        match settings::get::<AssistantSettings>(cx).dock {
 337            AssistantDockPosition::Left => DockPosition::Left,
 338            AssistantDockPosition::Bottom => DockPosition::Bottom,
 339            AssistantDockPosition::Right => DockPosition::Right,
 340        }
 341    }
 342
 343    fn position_is_valid(&self, _: DockPosition) -> bool {
 344        true
 345    }
 346
 347    fn set_position(&mut self, position: DockPosition, cx: &mut ViewContext<Self>) {
 348        settings::update_settings_file::<AssistantSettings>(self.fs.clone(), cx, move |settings| {
 349            let dock = match position {
 350                DockPosition::Left => AssistantDockPosition::Left,
 351                DockPosition::Bottom => AssistantDockPosition::Bottom,
 352                DockPosition::Right => AssistantDockPosition::Right,
 353            };
 354            settings.dock = Some(dock);
 355        });
 356    }
 357
 358    fn size(&self, cx: &WindowContext) -> f32 {
 359        let settings = settings::get::<AssistantSettings>(cx);
 360        match self.position(cx) {
 361            DockPosition::Left | DockPosition::Right => {
 362                self.width.unwrap_or_else(|| settings.default_width)
 363            }
 364            DockPosition::Bottom => self.height.unwrap_or_else(|| settings.default_height),
 365        }
 366    }
 367
 368    fn set_size(&mut self, size: f32, cx: &mut ViewContext<Self>) {
 369        match self.position(cx) {
 370            DockPosition::Left | DockPosition::Right => self.width = Some(size),
 371            DockPosition::Bottom => self.height = Some(size),
 372        }
 373        cx.notify();
 374    }
 375
 376    fn should_zoom_in_on_event(event: &AssistantPanelEvent) -> bool {
 377        matches!(event, AssistantPanelEvent::ZoomIn)
 378    }
 379
 380    fn should_zoom_out_on_event(event: &AssistantPanelEvent) -> bool {
 381        matches!(event, AssistantPanelEvent::ZoomOut)
 382    }
 383
 384    fn is_zoomed(&self, cx: &WindowContext) -> bool {
 385        self.pane.read(cx).is_zoomed()
 386    }
 387
 388    fn set_zoomed(&mut self, zoomed: bool, cx: &mut ViewContext<Self>) {
 389        self.pane.update(cx, |pane, cx| pane.set_zoomed(zoomed, cx));
 390    }
 391
 392    fn set_active(&mut self, active: bool, cx: &mut ViewContext<Self>) {
 393        if active {
 394            if self.api_key.borrow().is_none() && !self.has_read_credentials {
 395                self.has_read_credentials = true;
 396                let api_key = if let Ok(api_key) = env::var("OPENAI_API_KEY") {
 397                    Some(api_key)
 398                } else if let Some((_, api_key)) = cx
 399                    .platform()
 400                    .read_credentials(OPENAI_API_URL)
 401                    .log_err()
 402                    .flatten()
 403                {
 404                    String::from_utf8(api_key).log_err()
 405                } else {
 406                    None
 407                };
 408                if let Some(api_key) = api_key {
 409                    *self.api_key.borrow_mut() = Some(api_key);
 410                } else if self.api_key_editor.is_none() {
 411                    self.api_key_editor = Some(build_api_key_editor(cx));
 412                    cx.notify();
 413                }
 414            }
 415
 416            if self.pane.read(cx).items_len() == 0 {
 417                self.add_context(cx);
 418            }
 419        }
 420    }
 421
 422    fn icon_path(&self) -> &'static str {
 423        "icons/robot_14.svg"
 424    }
 425
 426    fn icon_tooltip(&self) -> (String, Option<Box<dyn Action>>) {
 427        ("Assistant Panel".into(), Some(Box::new(ToggleFocus)))
 428    }
 429
 430    fn should_change_position_on_event(event: &Self::Event) -> bool {
 431        matches!(event, AssistantPanelEvent::DockPositionChanged)
 432    }
 433
 434    fn should_activate_on_event(_: &Self::Event) -> bool {
 435        false
 436    }
 437
 438    fn should_close_on_event(event: &AssistantPanelEvent) -> bool {
 439        matches!(event, AssistantPanelEvent::Close)
 440    }
 441
 442    fn has_focus(&self, cx: &WindowContext) -> bool {
 443        self.pane.read(cx).has_focus()
 444            || self
 445                .api_key_editor
 446                .as_ref()
 447                .map_or(false, |editor| editor.is_focused(cx))
 448    }
 449
 450    fn is_focus_event(event: &Self::Event) -> bool {
 451        matches!(event, AssistantPanelEvent::Focus)
 452    }
 453}
 454
 455enum AssistantEvent {
 456    MessagesEdited,
 457    SummaryChanged,
 458    StreamedCompletion,
 459}
 460
 461#[derive(Clone, PartialEq, Eq)]
 462struct SavedConversationPath {
 463    path: PathBuf,
 464    had_summary: bool,
 465}
 466
 467struct Assistant {
 468    buffer: ModelHandle<Buffer>,
 469    message_anchors: Vec<MessageAnchor>,
 470    messages_metadata: HashMap<MessageId, MessageMetadata>,
 471    next_message_id: MessageId,
 472    summary: Option<String>,
 473    pending_summary: Task<Option<()>>,
 474    completion_count: usize,
 475    pending_completions: Vec<PendingCompletion>,
 476    model: String,
 477    token_count: Option<usize>,
 478    max_token_count: usize,
 479    pending_token_count: Task<Option<()>>,
 480    api_key: Rc<RefCell<Option<String>>>,
 481    pending_save: Task<Result<()>>,
 482    path: Option<SavedConversationPath>,
 483    _subscriptions: Vec<Subscription>,
 484}
 485
 486impl Entity for Assistant {
 487    type Event = AssistantEvent;
 488}
 489
 490impl Assistant {
 491    fn new(
 492        api_key: Rc<RefCell<Option<String>>>,
 493        language_registry: Arc<LanguageRegistry>,
 494        cx: &mut ModelContext<Self>,
 495    ) -> Self {
 496        let model = "gpt-3.5-turbo-0613";
 497        let markdown = language_registry.language_for_name("Markdown");
 498        let buffer = cx.add_model(|cx| {
 499            let mut buffer = Buffer::new(0, "", cx);
 500            buffer.set_language_registry(language_registry);
 501            cx.spawn_weak(|buffer, mut cx| async move {
 502                let markdown = markdown.await?;
 503                let buffer = buffer
 504                    .upgrade(&cx)
 505                    .ok_or_else(|| anyhow!("buffer was dropped"))?;
 506                buffer.update(&mut cx, |buffer, cx| {
 507                    buffer.set_language(Some(markdown), cx)
 508                });
 509                anyhow::Ok(())
 510            })
 511            .detach_and_log_err(cx);
 512            buffer
 513        });
 514
 515        let mut this = Self {
 516            message_anchors: Default::default(),
 517            messages_metadata: Default::default(),
 518            next_message_id: Default::default(),
 519            summary: None,
 520            pending_summary: Task::ready(None),
 521            completion_count: Default::default(),
 522            pending_completions: Default::default(),
 523            token_count: None,
 524            max_token_count: tiktoken_rs::model::get_context_size(model),
 525            pending_token_count: Task::ready(None),
 526            model: model.into(),
 527            _subscriptions: vec![cx.subscribe(&buffer, Self::handle_buffer_event)],
 528            pending_save: Task::ready(Ok(())),
 529            path: None,
 530            api_key,
 531            buffer,
 532        };
 533        let message = MessageAnchor {
 534            id: MessageId(post_inc(&mut this.next_message_id.0)),
 535            start: language::Anchor::MIN,
 536        };
 537        this.message_anchors.push(message.clone());
 538        this.messages_metadata.insert(
 539            message.id,
 540            MessageMetadata {
 541                role: Role::User,
 542                sent_at: Local::now(),
 543                status: MessageStatus::Done,
 544            },
 545        );
 546
 547        this.count_remaining_tokens(cx);
 548        this
 549    }
 550
 551    fn handle_buffer_event(
 552        &mut self,
 553        _: ModelHandle<Buffer>,
 554        event: &language::Event,
 555        cx: &mut ModelContext<Self>,
 556    ) {
 557        match event {
 558            language::Event::Edited => {
 559                self.count_remaining_tokens(cx);
 560                cx.emit(AssistantEvent::MessagesEdited);
 561            }
 562            _ => {}
 563        }
 564    }
 565
 566    fn count_remaining_tokens(&mut self, cx: &mut ModelContext<Self>) {
 567        let messages = self
 568            .messages(cx)
 569            .into_iter()
 570            .filter_map(|message| {
 571                Some(tiktoken_rs::ChatCompletionRequestMessage {
 572                    role: match message.role {
 573                        Role::User => "user".into(),
 574                        Role::Assistant => "assistant".into(),
 575                        Role::System => "system".into(),
 576                    },
 577                    content: self.buffer.read(cx).text_for_range(message.range).collect(),
 578                    name: None,
 579                })
 580            })
 581            .collect::<Vec<_>>();
 582        let model = self.model.clone();
 583        self.pending_token_count = cx.spawn_weak(|this, mut cx| {
 584            async move {
 585                cx.background().timer(Duration::from_millis(200)).await;
 586                let token_count = cx
 587                    .background()
 588                    .spawn(async move { tiktoken_rs::num_tokens_from_messages(&model, &messages) })
 589                    .await?;
 590
 591                this.upgrade(&cx)
 592                    .ok_or_else(|| anyhow!("assistant was dropped"))?
 593                    .update(&mut cx, |this, cx| {
 594                        this.max_token_count = tiktoken_rs::model::get_context_size(&this.model);
 595                        this.token_count = Some(token_count);
 596                        cx.notify()
 597                    });
 598                anyhow::Ok(())
 599            }
 600            .log_err()
 601        });
 602    }
 603
 604    fn remaining_tokens(&self) -> Option<isize> {
 605        Some(self.max_token_count as isize - self.token_count? as isize)
 606    }
 607
 608    fn set_model(&mut self, model: String, cx: &mut ModelContext<Self>) {
 609        self.model = model;
 610        self.count_remaining_tokens(cx);
 611        cx.notify();
 612    }
 613
 614    fn assist(
 615        &mut self,
 616        selected_messages: HashSet<MessageId>,
 617        cx: &mut ModelContext<Self>,
 618    ) -> Vec<MessageAnchor> {
 619        let mut user_messages = Vec::new();
 620        let mut tasks = Vec::new();
 621        for selected_message_id in selected_messages {
 622            let selected_message_role =
 623                if let Some(metadata) = self.messages_metadata.get(&selected_message_id) {
 624                    metadata.role
 625                } else {
 626                    continue;
 627                };
 628
 629            if selected_message_role == Role::Assistant {
 630                if let Some(user_message) = self.insert_message_after(
 631                    selected_message_id,
 632                    Role::User,
 633                    MessageStatus::Done,
 634                    cx,
 635                ) {
 636                    user_messages.push(user_message);
 637                } else {
 638                    continue;
 639                }
 640            } else {
 641                let request = OpenAIRequest {
 642                    model: self.model.clone(),
 643                    messages: self
 644                        .messages(cx)
 645                        .filter(|message| matches!(message.status, MessageStatus::Done))
 646                        .flat_map(|message| {
 647                            let mut system_message = None;
 648                            if message.id == selected_message_id {
 649                                system_message = Some(RequestMessage {
 650                                    role: Role::System,
 651                                    content: concat!(
 652                                        "Treat the following messages as additional knowledge you have learned about, ",
 653                                        "but act as if they were not part of this conversation. That is, treat them ",
 654                                        "as if the user didn't see them and couldn't possibly inquire about them."
 655                                    ).into()
 656                                });
 657                            }
 658
 659                            Some(message.to_open_ai_message(self.buffer.read(cx))).into_iter().chain(system_message)
 660                        })
 661                        .chain(Some(RequestMessage {
 662                            role: Role::System,
 663                            content: format!(
 664                                "Direct your reply to message with id {}. Do not include a [Message X] header.",
 665                                selected_message_id.0
 666                            ),
 667                        }))
 668                        .collect(),
 669                    stream: true,
 670                };
 671
 672                let Some(api_key) = self.api_key.borrow().clone() else { continue };
 673                let stream = stream_completion(api_key, cx.background().clone(), request);
 674                let assistant_message = self
 675                    .insert_message_after(
 676                        selected_message_id,
 677                        Role::Assistant,
 678                        MessageStatus::Pending,
 679                        cx,
 680                    )
 681                    .unwrap();
 682
 683                tasks.push(cx.spawn_weak({
 684                    |this, mut cx| async move {
 685                        let assistant_message_id = assistant_message.id;
 686                        let stream_completion = async {
 687                            let mut messages = stream.await?;
 688
 689                            while let Some(message) = messages.next().await {
 690                                let mut message = message?;
 691                                if let Some(choice) = message.choices.pop() {
 692                                    this.upgrade(&cx)
 693                                        .ok_or_else(|| anyhow!("assistant was dropped"))?
 694                                        .update(&mut cx, |this, cx| {
 695                                            let text: Arc<str> = choice.delta.content?.into();
 696                                            let message_ix = this.message_anchors.iter().position(
 697                                                |message| message.id == assistant_message_id,
 698                                            )?;
 699                                            this.buffer.update(cx, |buffer, cx| {
 700                                                let offset = this.message_anchors[message_ix + 1..]
 701                                                    .iter()
 702                                                    .find(|message| message.start.is_valid(buffer))
 703                                                    .map_or(buffer.len(), |message| {
 704                                                        message
 705                                                            .start
 706                                                            .to_offset(buffer)
 707                                                            .saturating_sub(1)
 708                                                    });
 709                                                buffer.edit([(offset..offset, text)], None, cx);
 710                                            });
 711                                            cx.emit(AssistantEvent::StreamedCompletion);
 712
 713                                            Some(())
 714                                        });
 715                                }
 716                                smol::future::yield_now().await;
 717                            }
 718
 719                            this.upgrade(&cx)
 720                                .ok_or_else(|| anyhow!("assistant was dropped"))?
 721                                .update(&mut cx, |this, cx| {
 722                                    this.pending_completions.retain(|completion| {
 723                                        completion.id != this.completion_count
 724                                    });
 725                                    this.summarize(cx);
 726                                });
 727
 728                            anyhow::Ok(())
 729                        };
 730
 731                        let result = stream_completion.await;
 732                        if let Some(this) = this.upgrade(&cx) {
 733                            this.update(&mut cx, |this, cx| {
 734                                if let Some(metadata) =
 735                                    this.messages_metadata.get_mut(&assistant_message.id)
 736                                {
 737                                    match result {
 738                                        Ok(_) => {
 739                                            metadata.status = MessageStatus::Done;
 740                                        }
 741                                        Err(error) => {
 742                                            metadata.status = MessageStatus::Error(
 743                                                error.to_string().trim().into(),
 744                                            );
 745                                        }
 746                                    }
 747                                    cx.notify();
 748                                }
 749                            });
 750                        }
 751                    }
 752                }));
 753            }
 754        }
 755
 756        if !tasks.is_empty() {
 757            self.pending_completions.push(PendingCompletion {
 758                id: post_inc(&mut self.completion_count),
 759                _tasks: tasks,
 760            });
 761        }
 762
 763        user_messages
 764    }
 765
 766    fn cancel_last_assist(&mut self) -> bool {
 767        self.pending_completions.pop().is_some()
 768    }
 769
 770    fn cycle_message_roles(&mut self, ids: HashSet<MessageId>, cx: &mut ModelContext<Self>) {
 771        for id in ids {
 772            if let Some(metadata) = self.messages_metadata.get_mut(&id) {
 773                metadata.role.cycle();
 774                cx.emit(AssistantEvent::MessagesEdited);
 775                cx.notify();
 776            }
 777        }
 778    }
 779
 780    fn insert_message_after(
 781        &mut self,
 782        message_id: MessageId,
 783        role: Role,
 784        status: MessageStatus,
 785        cx: &mut ModelContext<Self>,
 786    ) -> Option<MessageAnchor> {
 787        if let Some(prev_message_ix) = self
 788            .message_anchors
 789            .iter()
 790            .position(|message| message.id == message_id)
 791        {
 792            let start = self.buffer.update(cx, |buffer, cx| {
 793                let offset = self.message_anchors[prev_message_ix + 1..]
 794                    .iter()
 795                    .find(|message| message.start.is_valid(buffer))
 796                    .map_or(buffer.len(), |message| message.start.to_offset(buffer) - 1);
 797                buffer.edit([(offset..offset, "\n")], None, cx);
 798                buffer.anchor_before(offset + 1)
 799            });
 800            let message = MessageAnchor {
 801                id: MessageId(post_inc(&mut self.next_message_id.0)),
 802                start,
 803            };
 804            self.message_anchors
 805                .insert(prev_message_ix + 1, message.clone());
 806            self.messages_metadata.insert(
 807                message.id,
 808                MessageMetadata {
 809                    role,
 810                    sent_at: Local::now(),
 811                    status,
 812                },
 813            );
 814            cx.emit(AssistantEvent::MessagesEdited);
 815            Some(message)
 816        } else {
 817            None
 818        }
 819    }
 820
 821    fn split_message(
 822        &mut self,
 823        range: Range<usize>,
 824        cx: &mut ModelContext<Self>,
 825    ) -> (Option<MessageAnchor>, Option<MessageAnchor>) {
 826        let start_message = self.message_for_offset(range.start, cx);
 827        let end_message = self.message_for_offset(range.end, cx);
 828        if let Some((start_message, end_message)) = start_message.zip(end_message) {
 829            // Prevent splitting when range spans multiple messages.
 830            if start_message.index != end_message.index {
 831                return (None, None);
 832            }
 833
 834            let message = start_message;
 835            let role = message.role;
 836            let mut edited_buffer = false;
 837
 838            let mut suffix_start = None;
 839            if range.start > message.range.start && range.end < message.range.end - 1 {
 840                if self.buffer.read(cx).chars_at(range.end).next() == Some('\n') {
 841                    suffix_start = Some(range.end + 1);
 842                } else if self.buffer.read(cx).reversed_chars_at(range.end).next() == Some('\n') {
 843                    suffix_start = Some(range.end);
 844                }
 845            }
 846
 847            let suffix = if let Some(suffix_start) = suffix_start {
 848                MessageAnchor {
 849                    id: MessageId(post_inc(&mut self.next_message_id.0)),
 850                    start: self.buffer.read(cx).anchor_before(suffix_start),
 851                }
 852            } else {
 853                self.buffer.update(cx, |buffer, cx| {
 854                    buffer.edit([(range.end..range.end, "\n")], None, cx);
 855                });
 856                edited_buffer = true;
 857                MessageAnchor {
 858                    id: MessageId(post_inc(&mut self.next_message_id.0)),
 859                    start: self.buffer.read(cx).anchor_before(range.end + 1),
 860                }
 861            };
 862
 863            self.message_anchors
 864                .insert(message.index + 1, suffix.clone());
 865            self.messages_metadata.insert(
 866                suffix.id,
 867                MessageMetadata {
 868                    role,
 869                    sent_at: Local::now(),
 870                    status: MessageStatus::Done,
 871                },
 872            );
 873
 874            let new_messages = if range.start == range.end || range.start == message.range.start {
 875                (None, Some(suffix))
 876            } else {
 877                let mut prefix_end = None;
 878                if range.start > message.range.start && range.end < message.range.end - 1 {
 879                    if self.buffer.read(cx).chars_at(range.start).next() == Some('\n') {
 880                        prefix_end = Some(range.start + 1);
 881                    } else if self.buffer.read(cx).reversed_chars_at(range.start).next()
 882                        == Some('\n')
 883                    {
 884                        prefix_end = Some(range.start);
 885                    }
 886                }
 887
 888                let selection = if let Some(prefix_end) = prefix_end {
 889                    cx.emit(AssistantEvent::MessagesEdited);
 890                    MessageAnchor {
 891                        id: MessageId(post_inc(&mut self.next_message_id.0)),
 892                        start: self.buffer.read(cx).anchor_before(prefix_end),
 893                    }
 894                } else {
 895                    self.buffer.update(cx, |buffer, cx| {
 896                        buffer.edit([(range.start..range.start, "\n")], None, cx)
 897                    });
 898                    edited_buffer = true;
 899                    MessageAnchor {
 900                        id: MessageId(post_inc(&mut self.next_message_id.0)),
 901                        start: self.buffer.read(cx).anchor_before(range.end + 1),
 902                    }
 903                };
 904
 905                self.message_anchors
 906                    .insert(message.index + 1, selection.clone());
 907                self.messages_metadata.insert(
 908                    selection.id,
 909                    MessageMetadata {
 910                        role,
 911                        sent_at: Local::now(),
 912                        status: MessageStatus::Done,
 913                    },
 914                );
 915                (Some(selection), Some(suffix))
 916            };
 917
 918            if !edited_buffer {
 919                cx.emit(AssistantEvent::MessagesEdited);
 920            }
 921            new_messages
 922        } else {
 923            (None, None)
 924        }
 925    }
 926
 927    fn summarize(&mut self, cx: &mut ModelContext<Self>) {
 928        if self.message_anchors.len() >= 2 && self.summary.is_none() {
 929            let api_key = self.api_key.borrow().clone();
 930            if let Some(api_key) = api_key {
 931                let messages = self
 932                    .messages(cx)
 933                    .take(2)
 934                    .map(|message| message.to_open_ai_message(self.buffer.read(cx)))
 935                    .chain(Some(RequestMessage {
 936                        role: Role::User,
 937                        content:
 938                            "Summarize the conversation into a short title without punctuation"
 939                                .into(),
 940                    }));
 941                let request = OpenAIRequest {
 942                    model: self.model.clone(),
 943                    messages: messages.collect(),
 944                    stream: true,
 945                };
 946
 947                let stream = stream_completion(api_key, cx.background().clone(), request);
 948                self.pending_summary = cx.spawn(|this, mut cx| {
 949                    async move {
 950                        let mut messages = stream.await?;
 951
 952                        while let Some(message) = messages.next().await {
 953                            let mut message = message?;
 954                            if let Some(choice) = message.choices.pop() {
 955                                let text = choice.delta.content.unwrap_or_default();
 956                                this.update(&mut cx, |this, cx| {
 957                                    this.summary.get_or_insert(String::new()).push_str(&text);
 958                                    cx.emit(AssistantEvent::SummaryChanged);
 959                                });
 960                            }
 961                        }
 962
 963                        anyhow::Ok(())
 964                    }
 965                    .log_err()
 966                });
 967            }
 968        }
 969    }
 970
 971    fn message_for_offset(&self, offset: usize, cx: &AppContext) -> Option<Message> {
 972        self.messages_for_offsets([offset], cx).pop()
 973    }
 974
 975    fn messages_for_offsets(
 976        &self,
 977        offsets: impl IntoIterator<Item = usize>,
 978        cx: &AppContext,
 979    ) -> Vec<Message> {
 980        let mut result = Vec::new();
 981
 982        let buffer_len = self.buffer.read(cx).len();
 983        let mut messages = self.messages(cx).peekable();
 984        let mut offsets = offsets.into_iter().peekable();
 985        while let Some(offset) = offsets.next() {
 986            // Skip messages that start after the offset.
 987            while messages.peek().map_or(false, |message| {
 988                message.range.end < offset || (message.range.end == offset && offset < buffer_len)
 989            }) {
 990                messages.next();
 991            }
 992            let Some(message) = messages.peek() else { continue };
 993
 994            // Skip offsets that are in the same message.
 995            while offsets.peek().map_or(false, |offset| {
 996                message.range.contains(offset) || message.range.end == buffer_len
 997            }) {
 998                offsets.next();
 999            }
1000
1001            result.push(message.clone());
1002        }
1003        result
1004    }
1005
1006    fn messages<'a>(&'a self, cx: &'a AppContext) -> impl 'a + Iterator<Item = Message> {
1007        let buffer = self.buffer.read(cx);
1008        let mut message_anchors = self.message_anchors.iter().enumerate().peekable();
1009        iter::from_fn(move || {
1010            while let Some((ix, message_anchor)) = message_anchors.next() {
1011                let metadata = self.messages_metadata.get(&message_anchor.id)?;
1012                let message_start = message_anchor.start.to_offset(buffer);
1013                let mut message_end = None;
1014                while let Some((_, next_message)) = message_anchors.peek() {
1015                    if next_message.start.is_valid(buffer) {
1016                        message_end = Some(next_message.start);
1017                        break;
1018                    } else {
1019                        message_anchors.next();
1020                    }
1021                }
1022                let message_end = message_end
1023                    .unwrap_or(language::Anchor::MAX)
1024                    .to_offset(buffer);
1025                return Some(Message {
1026                    index: ix,
1027                    range: message_start..message_end,
1028                    id: message_anchor.id,
1029                    anchor: message_anchor.start,
1030                    role: metadata.role,
1031                    sent_at: metadata.sent_at,
1032                    status: metadata.status.clone(),
1033                });
1034            }
1035            None
1036        })
1037    }
1038
1039    fn save(
1040        &mut self,
1041        debounce: Option<Duration>,
1042        fs: Arc<dyn Fs>,
1043        cx: &mut ModelContext<Assistant>,
1044    ) {
1045        self.pending_save = cx.spawn(|this, mut cx| async move {
1046            if let Some(debounce) = debounce {
1047                cx.background().timer(debounce).await;
1048            }
1049            let conversation = SavedConversation {
1050                zed: "conversation".into(),
1051                version: "0.1".into(),
1052                messages: this.read_with(&cx, |this, cx| {
1053                    this.messages(cx)
1054                        .map(|message| message.to_open_ai_message(this.buffer.read(cx)))
1055                        .collect()
1056                }),
1057            };
1058
1059            let (old_path, summary) =
1060                this.read_with(&cx, |this, _| (this.path.clone(), this.summary.clone()));
1061            let mut new_path = None;
1062            if let Some(old_path) = old_path.as_ref() {
1063                if old_path.had_summary || summary.is_none() {
1064                    new_path = Some(old_path.clone());
1065                }
1066            }
1067
1068            let new_path = if let Some(new_path) = new_path {
1069                new_path
1070            } else {
1071                let mut path =
1072                    CONVERSATIONS_DIR.join(summary.as_deref().unwrap_or("conversation-1"));
1073
1074                while fs.is_file(&path).await {
1075                    let file_name = path.file_name().ok_or_else(|| anyhow!("no filename"))?;
1076                    let file_name = file_name.to_string_lossy();
1077
1078                    if let Some((prefix, suffix)) = file_name.rsplit_once('-') {
1079                        let new_version = suffix.parse::<u32>().ok().unwrap_or(1) + 1;
1080                        path.set_file_name(format!("{}-{}", prefix, new_version));
1081                    };
1082                }
1083
1084                SavedConversationPath {
1085                    path,
1086                    had_summary: summary.is_some(),
1087                }
1088            };
1089
1090            fs.create_dir(CONVERSATIONS_DIR.as_ref()).await?;
1091            fs.atomic_write(
1092                new_path.path.clone(),
1093                serde_json::to_string(&conversation).unwrap(),
1094            )
1095            .await?;
1096            this.update(&mut cx, |this, _| this.path = Some(new_path.clone()));
1097            if let Some(old_path) = old_path {
1098                if new_path.path != old_path.path {
1099                    fs.remove_file(
1100                        &old_path.path,
1101                        fs::RemoveOptions {
1102                            recursive: false,
1103                            ignore_if_not_exists: true,
1104                        },
1105                    )
1106                    .await?;
1107                }
1108            }
1109
1110            Ok(())
1111        });
1112    }
1113}
1114
1115struct PendingCompletion {
1116    id: usize,
1117    _tasks: Vec<Task<()>>,
1118}
1119
1120enum AssistantEditorEvent {
1121    TabContentChanged,
1122}
1123
1124#[derive(Copy, Clone, Debug, PartialEq)]
1125struct ScrollPosition {
1126    offset_before_cursor: Vector2F,
1127    cursor: Anchor,
1128}
1129
1130struct AssistantEditor {
1131    assistant: ModelHandle<Assistant>,
1132    fs: Arc<dyn Fs>,
1133    editor: ViewHandle<Editor>,
1134    blocks: HashSet<BlockId>,
1135    scroll_position: Option<ScrollPosition>,
1136    _subscriptions: Vec<Subscription>,
1137}
1138
1139impl AssistantEditor {
1140    fn new(
1141        api_key: Rc<RefCell<Option<String>>>,
1142        language_registry: Arc<LanguageRegistry>,
1143        fs: Arc<dyn Fs>,
1144        cx: &mut ViewContext<Self>,
1145    ) -> Self {
1146        let assistant = cx.add_model(|cx| Assistant::new(api_key, language_registry, cx));
1147        let editor = cx.add_view(|cx| {
1148            let mut editor = Editor::for_buffer(assistant.read(cx).buffer.clone(), None, cx);
1149            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
1150            editor.set_show_gutter(false, cx);
1151            editor
1152        });
1153
1154        let _subscriptions = vec![
1155            cx.observe(&assistant, |_, _, cx| cx.notify()),
1156            cx.subscribe(&assistant, Self::handle_assistant_event),
1157            cx.subscribe(&editor, Self::handle_editor_event),
1158        ];
1159
1160        let mut this = Self {
1161            assistant,
1162            editor,
1163            blocks: Default::default(),
1164            scroll_position: None,
1165            fs,
1166            _subscriptions,
1167        };
1168        this.update_message_headers(cx);
1169        this
1170    }
1171
1172    fn assist(&mut self, _: &Assist, cx: &mut ViewContext<Self>) {
1173        let cursors = self.cursors(cx);
1174
1175        let user_messages = self.assistant.update(cx, |assistant, cx| {
1176            let selected_messages = assistant
1177                .messages_for_offsets(cursors, cx)
1178                .into_iter()
1179                .map(|message| message.id)
1180                .collect();
1181            assistant.assist(selected_messages, cx)
1182        });
1183        let new_selections = user_messages
1184            .iter()
1185            .map(|message| {
1186                let cursor = message
1187                    .start
1188                    .to_offset(self.assistant.read(cx).buffer.read(cx));
1189                cursor..cursor
1190            })
1191            .collect::<Vec<_>>();
1192        if !new_selections.is_empty() {
1193            self.editor.update(cx, |editor, cx| {
1194                editor.change_selections(
1195                    Some(Autoscroll::Strategy(AutoscrollStrategy::Fit)),
1196                    cx,
1197                    |selections| selections.select_ranges(new_selections),
1198                );
1199            });
1200        }
1201    }
1202
1203    fn cancel_last_assist(&mut self, _: &editor::Cancel, cx: &mut ViewContext<Self>) {
1204        if !self
1205            .assistant
1206            .update(cx, |assistant, _| assistant.cancel_last_assist())
1207        {
1208            cx.propagate_action();
1209        }
1210    }
1211
1212    fn cycle_message_role(&mut self, _: &CycleMessageRole, cx: &mut ViewContext<Self>) {
1213        let cursors = self.cursors(cx);
1214        self.assistant.update(cx, |assistant, cx| {
1215            let messages = assistant
1216                .messages_for_offsets(cursors, cx)
1217                .into_iter()
1218                .map(|message| message.id)
1219                .collect();
1220            assistant.cycle_message_roles(messages, cx)
1221        });
1222    }
1223
1224    fn cursors(&self, cx: &AppContext) -> Vec<usize> {
1225        let selections = self.editor.read(cx).selections.all::<usize>(cx);
1226        selections
1227            .into_iter()
1228            .map(|selection| selection.head())
1229            .collect()
1230    }
1231
1232    fn handle_assistant_event(
1233        &mut self,
1234        _: ModelHandle<Assistant>,
1235        event: &AssistantEvent,
1236        cx: &mut ViewContext<Self>,
1237    ) {
1238        match event {
1239            AssistantEvent::MessagesEdited => {
1240                self.update_message_headers(cx);
1241                self.assistant.update(cx, |assistant, cx| {
1242                    assistant.save(Some(Duration::from_millis(500)), self.fs.clone(), cx);
1243                });
1244            }
1245            AssistantEvent::SummaryChanged => {
1246                cx.emit(AssistantEditorEvent::TabContentChanged);
1247                self.assistant.update(cx, |assistant, cx| {
1248                    assistant.save(None, self.fs.clone(), cx);
1249                });
1250            }
1251            AssistantEvent::StreamedCompletion => {
1252                self.editor.update(cx, |editor, cx| {
1253                    if let Some(scroll_position) = self.scroll_position {
1254                        let snapshot = editor.snapshot(cx);
1255                        let cursor_point = scroll_position.cursor.to_display_point(&snapshot);
1256                        let scroll_top =
1257                            cursor_point.row() as f32 - scroll_position.offset_before_cursor.y();
1258                        editor.set_scroll_position(
1259                            vec2f(scroll_position.offset_before_cursor.x(), scroll_top),
1260                            cx,
1261                        );
1262                    }
1263                });
1264            }
1265        }
1266    }
1267
1268    fn handle_editor_event(
1269        &mut self,
1270        _: ViewHandle<Editor>,
1271        event: &editor::Event,
1272        cx: &mut ViewContext<Self>,
1273    ) {
1274        match event {
1275            editor::Event::ScrollPositionChanged { autoscroll, .. } => {
1276                let cursor_scroll_position = self.cursor_scroll_position(cx);
1277                if *autoscroll {
1278                    self.scroll_position = cursor_scroll_position;
1279                } else if self.scroll_position != cursor_scroll_position {
1280                    self.scroll_position = None;
1281                }
1282            }
1283            editor::Event::SelectionsChanged { .. } => {
1284                self.scroll_position = self.cursor_scroll_position(cx);
1285            }
1286            _ => {}
1287        }
1288    }
1289
1290    fn cursor_scroll_position(&self, cx: &mut ViewContext<Self>) -> Option<ScrollPosition> {
1291        self.editor.update(cx, |editor, cx| {
1292            let snapshot = editor.snapshot(cx);
1293            let cursor = editor.selections.newest_anchor().head();
1294            let cursor_row = cursor.to_display_point(&snapshot.display_snapshot).row() as f32;
1295            let scroll_position = editor
1296                .scroll_manager
1297                .anchor()
1298                .scroll_position(&snapshot.display_snapshot);
1299
1300            let scroll_bottom = scroll_position.y() + editor.visible_line_count().unwrap_or(0.);
1301            if (scroll_position.y()..scroll_bottom).contains(&cursor_row) {
1302                Some(ScrollPosition {
1303                    cursor,
1304                    offset_before_cursor: vec2f(
1305                        scroll_position.x(),
1306                        cursor_row - scroll_position.y(),
1307                    ),
1308                })
1309            } else {
1310                None
1311            }
1312        })
1313    }
1314
1315    fn update_message_headers(&mut self, cx: &mut ViewContext<Self>) {
1316        self.editor.update(cx, |editor, cx| {
1317            let buffer = editor.buffer().read(cx).snapshot(cx);
1318            let excerpt_id = *buffer.as_singleton().unwrap().0;
1319            let old_blocks = std::mem::take(&mut self.blocks);
1320            let new_blocks = self
1321                .assistant
1322                .read(cx)
1323                .messages(cx)
1324                .map(|message| BlockProperties {
1325                    position: buffer.anchor_in_excerpt(excerpt_id, message.anchor),
1326                    height: 2,
1327                    style: BlockStyle::Sticky,
1328                    render: Arc::new({
1329                        let assistant = self.assistant.clone();
1330                        // let metadata = message.metadata.clone();
1331                        // let message = message.clone();
1332                        move |cx| {
1333                            enum Sender {}
1334                            enum ErrorTooltip {}
1335
1336                            let theme = theme::current(cx);
1337                            let style = &theme.assistant;
1338                            let message_id = message.id;
1339                            let sender = MouseEventHandler::<Sender, _>::new(
1340                                message_id.0,
1341                                cx,
1342                                |state, _| match message.role {
1343                                    Role::User => {
1344                                        let style = style.user_sender.style_for(state, false);
1345                                        Label::new("You", style.text.clone())
1346                                            .contained()
1347                                            .with_style(style.container)
1348                                    }
1349                                    Role::Assistant => {
1350                                        let style = style.assistant_sender.style_for(state, false);
1351                                        Label::new("Assistant", style.text.clone())
1352                                            .contained()
1353                                            .with_style(style.container)
1354                                    }
1355                                    Role::System => {
1356                                        let style = style.system_sender.style_for(state, false);
1357                                        Label::new("System", style.text.clone())
1358                                            .contained()
1359                                            .with_style(style.container)
1360                                    }
1361                                },
1362                            )
1363                            .with_cursor_style(CursorStyle::PointingHand)
1364                            .on_down(MouseButton::Left, {
1365                                let assistant = assistant.clone();
1366                                move |_, _, cx| {
1367                                    assistant.update(cx, |assistant, cx| {
1368                                        assistant.cycle_message_roles(
1369                                            HashSet::from_iter(Some(message_id)),
1370                                            cx,
1371                                        )
1372                                    })
1373                                }
1374                            });
1375
1376                            Flex::row()
1377                                .with_child(sender.aligned())
1378                                .with_child(
1379                                    Label::new(
1380                                        message.sent_at.format("%I:%M%P").to_string(),
1381                                        style.sent_at.text.clone(),
1382                                    )
1383                                    .contained()
1384                                    .with_style(style.sent_at.container)
1385                                    .aligned(),
1386                                )
1387                                .with_children(
1388                                    if let MessageStatus::Error(error) = &message.status {
1389                                        Some(
1390                                            Svg::new("icons/circle_x_mark_12.svg")
1391                                                .with_color(style.error_icon.color)
1392                                                .constrained()
1393                                                .with_width(style.error_icon.width)
1394                                                .contained()
1395                                                .with_style(style.error_icon.container)
1396                                                .with_tooltip::<ErrorTooltip>(
1397                                                    message_id.0,
1398                                                    error.to_string(),
1399                                                    None,
1400                                                    theme.tooltip.clone(),
1401                                                    cx,
1402                                                )
1403                                                .aligned(),
1404                                        )
1405                                    } else {
1406                                        None
1407                                    },
1408                                )
1409                                .aligned()
1410                                .left()
1411                                .contained()
1412                                .with_style(style.header)
1413                                .into_any()
1414                        }
1415                    }),
1416                    disposition: BlockDisposition::Above,
1417                })
1418                .collect::<Vec<_>>();
1419
1420            editor.remove_blocks(old_blocks, None, cx);
1421            let ids = editor.insert_blocks(new_blocks, None, cx);
1422            self.blocks = HashSet::from_iter(ids);
1423        });
1424    }
1425
1426    fn quote_selection(
1427        workspace: &mut Workspace,
1428        _: &QuoteSelection,
1429        cx: &mut ViewContext<Workspace>,
1430    ) {
1431        let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
1432            return;
1433        };
1434        let Some(editor) = workspace.active_item(cx).and_then(|item| item.downcast::<Editor>()) else {
1435            return;
1436        };
1437
1438        let text = editor.read_with(cx, |editor, cx| {
1439            let range = editor.selections.newest::<usize>(cx).range();
1440            let buffer = editor.buffer().read(cx).snapshot(cx);
1441            let start_language = buffer.language_at(range.start);
1442            let end_language = buffer.language_at(range.end);
1443            let language_name = if start_language == end_language {
1444                start_language.map(|language| language.name())
1445            } else {
1446                None
1447            };
1448            let language_name = language_name.as_deref().unwrap_or("").to_lowercase();
1449
1450            let selected_text = buffer.text_for_range(range).collect::<String>();
1451            if selected_text.is_empty() {
1452                None
1453            } else {
1454                Some(if language_name == "markdown" {
1455                    selected_text
1456                        .lines()
1457                        .map(|line| format!("> {}", line))
1458                        .collect::<Vec<_>>()
1459                        .join("\n")
1460                } else {
1461                    format!("```{language_name}\n{selected_text}\n```")
1462                })
1463            }
1464        });
1465
1466        // Activate the panel
1467        if !panel.read(cx).has_focus(cx) {
1468            workspace.toggle_panel_focus::<AssistantPanel>(cx);
1469        }
1470
1471        if let Some(text) = text {
1472            panel.update(cx, |panel, cx| {
1473                if let Some(assistant) = panel
1474                    .pane
1475                    .read(cx)
1476                    .active_item()
1477                    .and_then(|item| item.downcast::<AssistantEditor>())
1478                    .ok_or_else(|| anyhow!("no active context"))
1479                    .log_err()
1480                {
1481                    assistant.update(cx, |assistant, cx| {
1482                        assistant
1483                            .editor
1484                            .update(cx, |editor, cx| editor.insert(&text, cx))
1485                    });
1486                }
1487            });
1488        }
1489    }
1490
1491    fn copy(&mut self, _: &editor::Copy, cx: &mut ViewContext<Self>) {
1492        let editor = self.editor.read(cx);
1493        let assistant = self.assistant.read(cx);
1494        if editor.selections.count() == 1 {
1495            let selection = editor.selections.newest::<usize>(cx);
1496            let mut copied_text = String::new();
1497            let mut spanned_messages = 0;
1498            for message in assistant.messages(cx) {
1499                if message.range.start >= selection.range().end {
1500                    break;
1501                } else if message.range.end >= selection.range().start {
1502                    let range = cmp::max(message.range.start, selection.range().start)
1503                        ..cmp::min(message.range.end, selection.range().end);
1504                    if !range.is_empty() {
1505                        spanned_messages += 1;
1506                        write!(&mut copied_text, "## {}\n\n", message.role).unwrap();
1507                        for chunk in assistant.buffer.read(cx).text_for_range(range) {
1508                            copied_text.push_str(&chunk);
1509                        }
1510                        copied_text.push('\n');
1511                    }
1512                }
1513            }
1514
1515            if spanned_messages > 1 {
1516                cx.platform()
1517                    .write_to_clipboard(ClipboardItem::new(copied_text));
1518                return;
1519            }
1520        }
1521
1522        cx.propagate_action();
1523    }
1524
1525    fn split(&mut self, _: &Split, cx: &mut ViewContext<Self>) {
1526        self.assistant.update(cx, |assistant, cx| {
1527            let selections = self.editor.read(cx).selections.disjoint_anchors();
1528            for selection in selections.into_iter() {
1529                let buffer = self.editor.read(cx).buffer().read(cx).snapshot(cx);
1530                let range = selection
1531                    .map(|endpoint| endpoint.to_offset(&buffer))
1532                    .range();
1533                assistant.split_message(range, cx);
1534            }
1535        });
1536    }
1537
1538    fn save(&mut self, _: &Save, cx: &mut ViewContext<Self>) {
1539        self.assistant.update(cx, |assistant, cx| {
1540            assistant.save(None, self.fs.clone(), cx)
1541        });
1542    }
1543
1544    fn cycle_model(&mut self, cx: &mut ViewContext<Self>) {
1545        self.assistant.update(cx, |assistant, cx| {
1546            let new_model = match assistant.model.as_str() {
1547                "gpt-4-0613" => "gpt-3.5-turbo-0613",
1548                _ => "gpt-4-0613",
1549            };
1550            assistant.set_model(new_model.into(), cx);
1551        });
1552    }
1553
1554    fn title(&self, cx: &AppContext) -> String {
1555        self.assistant
1556            .read(cx)
1557            .summary
1558            .clone()
1559            .unwrap_or_else(|| "New Context".into())
1560    }
1561}
1562
1563impl Entity for AssistantEditor {
1564    type Event = AssistantEditorEvent;
1565}
1566
1567impl View for AssistantEditor {
1568    fn ui_name() -> &'static str {
1569        "AssistantEditor"
1570    }
1571
1572    fn render(&mut self, cx: &mut ViewContext<Self>) -> AnyElement<Self> {
1573        enum Model {}
1574        let theme = &theme::current(cx).assistant;
1575        let assistant = &self.assistant.read(cx);
1576        let model = assistant.model.clone();
1577        let remaining_tokens = assistant.remaining_tokens().map(|remaining_tokens| {
1578            let remaining_tokens_style = if remaining_tokens <= 0 {
1579                &theme.no_remaining_tokens
1580            } else {
1581                &theme.remaining_tokens
1582            };
1583            Label::new(
1584                remaining_tokens.to_string(),
1585                remaining_tokens_style.text.clone(),
1586            )
1587            .contained()
1588            .with_style(remaining_tokens_style.container)
1589        });
1590
1591        Stack::new()
1592            .with_child(
1593                ChildView::new(&self.editor, cx)
1594                    .contained()
1595                    .with_style(theme.container),
1596            )
1597            .with_child(
1598                Flex::row()
1599                    .with_child(
1600                        MouseEventHandler::<Model, _>::new(0, cx, |state, _| {
1601                            let style = theme.model.style_for(state, false);
1602                            Label::new(model, style.text.clone())
1603                                .contained()
1604                                .with_style(style.container)
1605                        })
1606                        .with_cursor_style(CursorStyle::PointingHand)
1607                        .on_click(MouseButton::Left, |_, this, cx| this.cycle_model(cx)),
1608                    )
1609                    .with_children(remaining_tokens)
1610                    .contained()
1611                    .with_style(theme.model_info_container)
1612                    .aligned()
1613                    .top()
1614                    .right(),
1615            )
1616            .into_any()
1617    }
1618
1619    fn focus_in(&mut self, _: gpui::AnyViewHandle, cx: &mut ViewContext<Self>) {
1620        if cx.is_self_focused() {
1621            cx.focus(&self.editor);
1622        }
1623    }
1624}
1625
1626impl Item for AssistantEditor {
1627    fn tab_content<V: View>(
1628        &self,
1629        _: Option<usize>,
1630        style: &theme::Tab,
1631        cx: &gpui::AppContext,
1632    ) -> AnyElement<V> {
1633        let title = truncate_and_trailoff(&self.title(cx), editor::MAX_TAB_TITLE_LEN);
1634        Label::new(title, style.label.clone()).into_any()
1635    }
1636
1637    fn tab_tooltip_text(&self, cx: &AppContext) -> Option<Cow<str>> {
1638        Some(self.title(cx).into())
1639    }
1640
1641    fn as_searchable(
1642        &self,
1643        _: &ViewHandle<Self>,
1644    ) -> Option<Box<dyn workspace::searchable::SearchableItemHandle>> {
1645        Some(Box::new(self.editor.clone()))
1646    }
1647}
1648
1649#[derive(Copy, Clone, Debug, Default, Eq, PartialEq, Hash)]
1650struct MessageId(usize);
1651
1652#[derive(Clone, Debug)]
1653struct MessageAnchor {
1654    id: MessageId,
1655    start: language::Anchor,
1656}
1657
1658#[derive(Clone, Debug)]
1659struct MessageMetadata {
1660    role: Role,
1661    sent_at: DateTime<Local>,
1662    status: MessageStatus,
1663}
1664
1665#[derive(Clone, Debug)]
1666enum MessageStatus {
1667    Pending,
1668    Done,
1669    Error(Arc<str>),
1670}
1671
1672#[derive(Clone, Debug)]
1673pub struct Message {
1674    range: Range<usize>,
1675    index: usize,
1676    id: MessageId,
1677    anchor: language::Anchor,
1678    role: Role,
1679    sent_at: DateTime<Local>,
1680    status: MessageStatus,
1681}
1682
1683impl Message {
1684    fn to_open_ai_message(&self, buffer: &Buffer) -> RequestMessage {
1685        let mut content = format!("[Message {}]\n", self.id.0).to_string();
1686        content.extend(buffer.text_for_range(self.range.clone()));
1687        RequestMessage {
1688            role: self.role,
1689            content,
1690        }
1691    }
1692}
1693
1694async fn stream_completion(
1695    api_key: String,
1696    executor: Arc<Background>,
1697    mut request: OpenAIRequest,
1698) -> Result<impl Stream<Item = Result<OpenAIResponseStreamEvent>>> {
1699    request.stream = true;
1700
1701    let (tx, rx) = futures::channel::mpsc::unbounded::<Result<OpenAIResponseStreamEvent>>();
1702
1703    let json_data = serde_json::to_string(&request)?;
1704    let mut response = Request::post(format!("{OPENAI_API_URL}/chat/completions"))
1705        .header("Content-Type", "application/json")
1706        .header("Authorization", format!("Bearer {}", api_key))
1707        .body(json_data)?
1708        .send_async()
1709        .await?;
1710
1711    let status = response.status();
1712    if status == StatusCode::OK {
1713        executor
1714            .spawn(async move {
1715                let mut lines = BufReader::new(response.body_mut()).lines();
1716
1717                fn parse_line(
1718                    line: Result<String, io::Error>,
1719                ) -> Result<Option<OpenAIResponseStreamEvent>> {
1720                    if let Some(data) = line?.strip_prefix("data: ") {
1721                        let event = serde_json::from_str(&data)?;
1722                        Ok(Some(event))
1723                    } else {
1724                        Ok(None)
1725                    }
1726                }
1727
1728                while let Some(line) = lines.next().await {
1729                    if let Some(event) = parse_line(line).transpose() {
1730                        let done = event.as_ref().map_or(false, |event| {
1731                            event
1732                                .choices
1733                                .last()
1734                                .map_or(false, |choice| choice.finish_reason.is_some())
1735                        });
1736                        if tx.unbounded_send(event).is_err() {
1737                            break;
1738                        }
1739
1740                        if done {
1741                            break;
1742                        }
1743                    }
1744                }
1745
1746                anyhow::Ok(())
1747            })
1748            .detach();
1749
1750        Ok(rx)
1751    } else {
1752        let mut body = String::new();
1753        response.body_mut().read_to_string(&mut body).await?;
1754
1755        #[derive(Deserialize)]
1756        struct OpenAIResponse {
1757            error: OpenAIError,
1758        }
1759
1760        #[derive(Deserialize)]
1761        struct OpenAIError {
1762            message: String,
1763        }
1764
1765        match serde_json::from_str::<OpenAIResponse>(&body) {
1766            Ok(response) if !response.error.message.is_empty() => Err(anyhow!(
1767                "Failed to connect to OpenAI API: {}",
1768                response.error.message,
1769            )),
1770
1771            _ => Err(anyhow!(
1772                "Failed to connect to OpenAI API: {} {}",
1773                response.status(),
1774                body,
1775            )),
1776        }
1777    }
1778}
1779
1780#[cfg(test)]
1781mod tests {
1782    use super::*;
1783    use gpui::AppContext;
1784
1785    #[gpui::test]
1786    fn test_inserting_and_removing_messages(cx: &mut AppContext) {
1787        let registry = Arc::new(LanguageRegistry::test());
1788        let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx));
1789        let buffer = assistant.read(cx).buffer.clone();
1790
1791        let message_1 = assistant.read(cx).message_anchors[0].clone();
1792        assert_eq!(
1793            messages(&assistant, cx),
1794            vec![(message_1.id, Role::User, 0..0)]
1795        );
1796
1797        let message_2 = assistant.update(cx, |assistant, cx| {
1798            assistant
1799                .insert_message_after(message_1.id, Role::Assistant, MessageStatus::Done, cx)
1800                .unwrap()
1801        });
1802        assert_eq!(
1803            messages(&assistant, cx),
1804            vec![
1805                (message_1.id, Role::User, 0..1),
1806                (message_2.id, Role::Assistant, 1..1)
1807            ]
1808        );
1809
1810        buffer.update(cx, |buffer, cx| {
1811            buffer.edit([(0..0, "1"), (1..1, "2")], None, cx)
1812        });
1813        assert_eq!(
1814            messages(&assistant, cx),
1815            vec![
1816                (message_1.id, Role::User, 0..2),
1817                (message_2.id, Role::Assistant, 2..3)
1818            ]
1819        );
1820
1821        let message_3 = assistant.update(cx, |assistant, cx| {
1822            assistant
1823                .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
1824                .unwrap()
1825        });
1826        assert_eq!(
1827            messages(&assistant, cx),
1828            vec![
1829                (message_1.id, Role::User, 0..2),
1830                (message_2.id, Role::Assistant, 2..4),
1831                (message_3.id, Role::User, 4..4)
1832            ]
1833        );
1834
1835        let message_4 = assistant.update(cx, |assistant, cx| {
1836            assistant
1837                .insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
1838                .unwrap()
1839        });
1840        assert_eq!(
1841            messages(&assistant, cx),
1842            vec![
1843                (message_1.id, Role::User, 0..2),
1844                (message_2.id, Role::Assistant, 2..4),
1845                (message_4.id, Role::User, 4..5),
1846                (message_3.id, Role::User, 5..5),
1847            ]
1848        );
1849
1850        buffer.update(cx, |buffer, cx| {
1851            buffer.edit([(4..4, "C"), (5..5, "D")], None, cx)
1852        });
1853        assert_eq!(
1854            messages(&assistant, cx),
1855            vec![
1856                (message_1.id, Role::User, 0..2),
1857                (message_2.id, Role::Assistant, 2..4),
1858                (message_4.id, Role::User, 4..6),
1859                (message_3.id, Role::User, 6..7),
1860            ]
1861        );
1862
1863        // Deleting across message boundaries merges the messages.
1864        buffer.update(cx, |buffer, cx| buffer.edit([(1..4, "")], None, cx));
1865        assert_eq!(
1866            messages(&assistant, cx),
1867            vec![
1868                (message_1.id, Role::User, 0..3),
1869                (message_3.id, Role::User, 3..4),
1870            ]
1871        );
1872
1873        // Undoing the deletion should also undo the merge.
1874        buffer.update(cx, |buffer, cx| buffer.undo(cx));
1875        assert_eq!(
1876            messages(&assistant, cx),
1877            vec![
1878                (message_1.id, Role::User, 0..2),
1879                (message_2.id, Role::Assistant, 2..4),
1880                (message_4.id, Role::User, 4..6),
1881                (message_3.id, Role::User, 6..7),
1882            ]
1883        );
1884
1885        // Redoing the deletion should also redo the merge.
1886        buffer.update(cx, |buffer, cx| buffer.redo(cx));
1887        assert_eq!(
1888            messages(&assistant, cx),
1889            vec![
1890                (message_1.id, Role::User, 0..3),
1891                (message_3.id, Role::User, 3..4),
1892            ]
1893        );
1894
1895        // Ensure we can still insert after a merged message.
1896        let message_5 = assistant.update(cx, |assistant, cx| {
1897            assistant
1898                .insert_message_after(message_1.id, Role::System, MessageStatus::Done, cx)
1899                .unwrap()
1900        });
1901        assert_eq!(
1902            messages(&assistant, cx),
1903            vec![
1904                (message_1.id, Role::User, 0..3),
1905                (message_5.id, Role::System, 3..4),
1906                (message_3.id, Role::User, 4..5)
1907            ]
1908        );
1909    }
1910
1911    #[gpui::test]
1912    fn test_message_splitting(cx: &mut AppContext) {
1913        let registry = Arc::new(LanguageRegistry::test());
1914        let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx));
1915        let buffer = assistant.read(cx).buffer.clone();
1916
1917        let message_1 = assistant.read(cx).message_anchors[0].clone();
1918        assert_eq!(
1919            messages(&assistant, cx),
1920            vec![(message_1.id, Role::User, 0..0)]
1921        );
1922
1923        buffer.update(cx, |buffer, cx| {
1924            buffer.edit([(0..0, "aaa\nbbb\nccc\nddd\n")], None, cx)
1925        });
1926
1927        let (_, message_2) =
1928            assistant.update(cx, |assistant, cx| assistant.split_message(3..3, cx));
1929        let message_2 = message_2.unwrap();
1930
1931        // We recycle newlines in the middle of a split message
1932        assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc\nddd\n");
1933        assert_eq!(
1934            messages(&assistant, cx),
1935            vec![
1936                (message_1.id, Role::User, 0..4),
1937                (message_2.id, Role::User, 4..16),
1938            ]
1939        );
1940
1941        let (_, message_3) =
1942            assistant.update(cx, |assistant, cx| assistant.split_message(3..3, cx));
1943        let message_3 = message_3.unwrap();
1944
1945        // We don't recycle newlines at the end of a split message
1946        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
1947        assert_eq!(
1948            messages(&assistant, cx),
1949            vec![
1950                (message_1.id, Role::User, 0..4),
1951                (message_3.id, Role::User, 4..5),
1952                (message_2.id, Role::User, 5..17),
1953            ]
1954        );
1955
1956        let (_, message_4) =
1957            assistant.update(cx, |assistant, cx| assistant.split_message(9..9, cx));
1958        let message_4 = message_4.unwrap();
1959        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\nccc\nddd\n");
1960        assert_eq!(
1961            messages(&assistant, cx),
1962            vec![
1963                (message_1.id, Role::User, 0..4),
1964                (message_3.id, Role::User, 4..5),
1965                (message_2.id, Role::User, 5..9),
1966                (message_4.id, Role::User, 9..17),
1967            ]
1968        );
1969
1970        let (_, message_5) =
1971            assistant.update(cx, |assistant, cx| assistant.split_message(9..9, cx));
1972        let message_5 = message_5.unwrap();
1973        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\nddd\n");
1974        assert_eq!(
1975            messages(&assistant, cx),
1976            vec![
1977                (message_1.id, Role::User, 0..4),
1978                (message_3.id, Role::User, 4..5),
1979                (message_2.id, Role::User, 5..9),
1980                (message_4.id, Role::User, 9..10),
1981                (message_5.id, Role::User, 10..18),
1982            ]
1983        );
1984
1985        let (message_6, message_7) =
1986            assistant.update(cx, |assistant, cx| assistant.split_message(14..16, cx));
1987        let message_6 = message_6.unwrap();
1988        let message_7 = message_7.unwrap();
1989        assert_eq!(buffer.read(cx).text(), "aaa\n\nbbb\n\nccc\ndd\nd\n");
1990        assert_eq!(
1991            messages(&assistant, cx),
1992            vec![
1993                (message_1.id, Role::User, 0..4),
1994                (message_3.id, Role::User, 4..5),
1995                (message_2.id, Role::User, 5..9),
1996                (message_4.id, Role::User, 9..10),
1997                (message_5.id, Role::User, 10..14),
1998                (message_6.id, Role::User, 14..17),
1999                (message_7.id, Role::User, 17..19),
2000            ]
2001        );
2002    }
2003
2004    #[gpui::test]
2005    fn test_messages_for_offsets(cx: &mut AppContext) {
2006        let registry = Arc::new(LanguageRegistry::test());
2007        let assistant = cx.add_model(|cx| Assistant::new(Default::default(), registry, cx));
2008        let buffer = assistant.read(cx).buffer.clone();
2009
2010        let message_1 = assistant.read(cx).message_anchors[0].clone();
2011        assert_eq!(
2012            messages(&assistant, cx),
2013            vec![(message_1.id, Role::User, 0..0)]
2014        );
2015
2016        buffer.update(cx, |buffer, cx| buffer.edit([(0..0, "aaa")], None, cx));
2017        let message_2 = assistant
2018            .update(cx, |assistant, cx| {
2019                assistant.insert_message_after(message_1.id, Role::User, MessageStatus::Done, cx)
2020            })
2021            .unwrap();
2022        buffer.update(cx, |buffer, cx| buffer.edit([(4..4, "bbb")], None, cx));
2023
2024        let message_3 = assistant
2025            .update(cx, |assistant, cx| {
2026                assistant.insert_message_after(message_2.id, Role::User, MessageStatus::Done, cx)
2027            })
2028            .unwrap();
2029        buffer.update(cx, |buffer, cx| buffer.edit([(8..8, "ccc")], None, cx));
2030
2031        assert_eq!(buffer.read(cx).text(), "aaa\nbbb\nccc");
2032        assert_eq!(
2033            messages(&assistant, cx),
2034            vec![
2035                (message_1.id, Role::User, 0..4),
2036                (message_2.id, Role::User, 4..8),
2037                (message_3.id, Role::User, 8..11)
2038            ]
2039        );
2040
2041        assert_eq!(
2042            message_ids_for_offsets(&assistant, &[0, 4, 9], cx),
2043            [message_1.id, message_2.id, message_3.id]
2044        );
2045        assert_eq!(
2046            message_ids_for_offsets(&assistant, &[0, 1, 11], cx),
2047            [message_1.id, message_3.id]
2048        );
2049
2050        fn message_ids_for_offsets(
2051            assistant: &ModelHandle<Assistant>,
2052            offsets: &[usize],
2053            cx: &AppContext,
2054        ) -> Vec<MessageId> {
2055            assistant
2056                .read(cx)
2057                .messages_for_offsets(offsets.iter().copied(), cx)
2058                .into_iter()
2059                .map(|message| message.id)
2060                .collect()
2061        }
2062    }
2063
2064    fn messages(
2065        assistant: &ModelHandle<Assistant>,
2066        cx: &AppContext,
2067    ) -> Vec<(MessageId, Role, Range<usize>)> {
2068        assistant
2069            .read(cx)
2070            .messages(cx)
2071            .map(|message| (message.id, message.role, message.range))
2072            .collect()
2073    }
2074}