prompt_library.rs

   1use crate::{
   2    slash_command::SlashCommandCompletionProvider, AssistantPanel, InlineAssist, InlineAssistant,
   3    LanguageModelCompletionProvider,
   4};
   5use anyhow::{anyhow, Result};
   6use assets::Assets;
   7use chrono::{DateTime, Utc};
   8use collections::{HashMap, HashSet};
   9use editor::{actions::Tab, CurrentLineHighlight, Editor, EditorElement, EditorEvent, EditorStyle};
  10use futures::{
  11    future::{self, BoxFuture, Shared},
  12    FutureExt,
  13};
  14use fuzzy::StringMatchCandidate;
  15use gpui::{
  16    actions, point, size, transparent_black, AppContext, AssetSource, BackgroundExecutor, Bounds,
  17    EventEmitter, Global, HighlightStyle, PromptLevel, ReadGlobal, Subscription, Task, TextStyle,
  18    TitlebarOptions, UpdateGlobal, View, WindowBounds, WindowHandle, WindowOptions,
  19};
  20use heed::{types::SerdeBincode, Database, RoTxn};
  21use language::{language_settings::SoftWrap, Buffer, LanguageRegistry};
  22use language_model::{LanguageModelRequest, LanguageModelRequestMessage, Role};
  23use parking_lot::RwLock;
  24use picker::{Picker, PickerDelegate};
  25use rope::Rope;
  26use serde::{Deserialize, Serialize};
  27use settings::Settings;
  28use std::{
  29    cmp::Reverse,
  30    future::Future,
  31    path::PathBuf,
  32    sync::{atomic::AtomicBool, Arc},
  33    time::Duration,
  34};
  35use theme::ThemeSettings;
  36use ui::{
  37    div, prelude::*, IconButtonShape, ListItem, ListItemSpacing, ParentElement, Render,
  38    SharedString, Styled, Tooltip, ViewContext, VisualContext,
  39};
  40use util::{ResultExt, TryFutureExt};
  41use uuid::Uuid;
  42use workspace::Workspace;
  43
  44actions!(
  45    prompt_library,
  46    [
  47        NewPrompt,
  48        DeletePrompt,
  49        DuplicatePrompt,
  50        ToggleDefaultPrompt
  51    ]
  52);
  53
  54/// Init starts loading the PromptStore in the background and assigns
  55/// a shared future to a global.
  56pub fn init(cx: &mut AppContext) {
  57    let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb");
  58    let prompt_store_future = PromptStore::new(db_path, cx.background_executor().clone())
  59        .then(|result| future::ready(result.map(Arc::new).map_err(Arc::new)))
  60        .boxed()
  61        .shared();
  62    cx.set_global(GlobalPromptStore(prompt_store_future))
  63}
  64
  65/// This function opens a new prompt library window if one doesn't exist already.
  66/// If one exists, it brings it to the foreground.
  67///
  68/// Note that, when opening a new window, this waits for the PromptStore to be
  69/// initialized. If it was initialized successfully, it returns a window handle
  70/// to a prompt library.
  71pub fn open_prompt_library(
  72    language_registry: Arc<LanguageRegistry>,
  73    cx: &mut AppContext,
  74) -> Task<Result<WindowHandle<PromptLibrary>>> {
  75    let existing_window = cx
  76        .windows()
  77        .into_iter()
  78        .find_map(|window| window.downcast::<PromptLibrary>());
  79    if let Some(existing_window) = existing_window {
  80        existing_window
  81            .update(cx, |_, cx| cx.activate_window())
  82            .ok();
  83        Task::ready(Ok(existing_window))
  84    } else {
  85        let store = PromptStore::global(cx);
  86        cx.spawn(|cx| async move {
  87            let store = store.await?;
  88            cx.update(|cx| {
  89                let bounds = Bounds::centered(None, size(px(1024.0), px(768.0)), cx);
  90                cx.open_window(
  91                    WindowOptions {
  92                        titlebar: Some(TitlebarOptions {
  93                            title: Some("Prompt Library".into()),
  94                            appears_transparent: true,
  95                            traffic_light_position: Some(point(px(9.0), px(9.0))),
  96                        }),
  97                        window_bounds: Some(WindowBounds::Windowed(bounds)),
  98                        ..Default::default()
  99                    },
 100                    |cx| cx.new_view(|cx| PromptLibrary::new(store, language_registry, cx)),
 101                )
 102            })?
 103        })
 104    }
 105}
 106
 107pub struct PromptLibrary {
 108    store: Arc<PromptStore>,
 109    language_registry: Arc<LanguageRegistry>,
 110    prompt_editors: HashMap<PromptId, PromptEditor>,
 111    active_prompt_id: Option<PromptId>,
 112    picker: View<Picker<PromptPickerDelegate>>,
 113    pending_load: Task<()>,
 114    _subscriptions: Vec<Subscription>,
 115}
 116
 117struct PromptEditor {
 118    title_editor: View<Editor>,
 119    body_editor: View<Editor>,
 120    token_count: Option<usize>,
 121    pending_token_count: Task<Option<()>>,
 122    next_title_and_body_to_save: Option<(String, Rope)>,
 123    pending_save: Option<Task<Option<()>>>,
 124    _subscriptions: Vec<Subscription>,
 125}
 126
 127struct PromptPickerDelegate {
 128    store: Arc<PromptStore>,
 129    selected_index: usize,
 130    matches: Vec<PromptMetadata>,
 131}
 132
 133enum PromptPickerEvent {
 134    Selected { prompt_id: PromptId },
 135    Confirmed { prompt_id: PromptId },
 136    Deleted { prompt_id: PromptId },
 137    ToggledDefault { prompt_id: PromptId },
 138}
 139
 140impl EventEmitter<PromptPickerEvent> for Picker<PromptPickerDelegate> {}
 141
 142impl PickerDelegate for PromptPickerDelegate {
 143    type ListItem = ListItem;
 144
 145    fn match_count(&self) -> usize {
 146        self.matches.len()
 147    }
 148
 149    fn selected_index(&self) -> usize {
 150        self.selected_index
 151    }
 152
 153    fn set_selected_index(&mut self, ix: usize, cx: &mut ViewContext<Picker<Self>>) {
 154        self.selected_index = ix;
 155        if let Some(prompt) = self.matches.get(self.selected_index) {
 156            cx.emit(PromptPickerEvent::Selected {
 157                prompt_id: prompt.id,
 158            });
 159        }
 160    }
 161
 162    fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
 163        "Search...".into()
 164    }
 165
 166    fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
 167        let search = self.store.search(query);
 168        let prev_prompt_id = self.matches.get(self.selected_index).map(|mat| mat.id);
 169        cx.spawn(|this, mut cx| async move {
 170            let (matches, selected_index) = cx
 171                .background_executor()
 172                .spawn(async move {
 173                    let matches = search.await;
 174
 175                    let selected_index = prev_prompt_id
 176                        .and_then(|prev_prompt_id| {
 177                            matches.iter().position(|entry| entry.id == prev_prompt_id)
 178                        })
 179                        .unwrap_or(0);
 180                    (matches, selected_index)
 181                })
 182                .await;
 183
 184            this.update(&mut cx, |this, cx| {
 185                this.delegate.matches = matches;
 186                this.delegate.set_selected_index(selected_index, cx);
 187                cx.notify();
 188            })
 189            .ok();
 190        })
 191    }
 192
 193    fn confirm(&mut self, _secondary: bool, cx: &mut ViewContext<Picker<Self>>) {
 194        if let Some(prompt) = self.matches.get(self.selected_index) {
 195            cx.emit(PromptPickerEvent::Confirmed {
 196                prompt_id: prompt.id,
 197            });
 198        }
 199    }
 200
 201    fn dismissed(&mut self, _cx: &mut ViewContext<Picker<Self>>) {}
 202
 203    fn render_match(
 204        &self,
 205        ix: usize,
 206        selected: bool,
 207        cx: &mut ViewContext<Picker<Self>>,
 208    ) -> Option<Self::ListItem> {
 209        let prompt = self.matches.get(ix)?;
 210        let default = prompt.default;
 211        let prompt_id = prompt.id;
 212        let element = ListItem::new(ix)
 213            .inset(true)
 214            .spacing(ListItemSpacing::Sparse)
 215            .selected(selected)
 216            .child(h_flex().h_5().line_height(relative(1.)).child(Label::new(
 217                prompt.title.clone().unwrap_or("Untitled".into()),
 218            )))
 219            .end_slot::<IconButton>(default.then(|| {
 220                IconButton::new("toggle-default-prompt", IconName::SparkleFilled)
 221                    .selected(true)
 222                    .icon_color(Color::Accent)
 223                    .shape(IconButtonShape::Square)
 224                    .tooltip(move |cx| Tooltip::text("Remove from Default Prompt", cx))
 225                    .on_click(cx.listener(move |_, _, cx| {
 226                        cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
 227                    }))
 228            }))
 229            .end_hover_slot(
 230                h_flex()
 231                    .gap_2()
 232                    .child(
 233                        IconButton::new("delete-prompt", IconName::Trash)
 234                            .icon_color(Color::Muted)
 235                            .shape(IconButtonShape::Square)
 236                            .tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
 237                            .on_click(cx.listener(move |_, _, cx| {
 238                                cx.emit(PromptPickerEvent::Deleted { prompt_id })
 239                            })),
 240                    )
 241                    .child(
 242                        IconButton::new("toggle-default-prompt", IconName::Sparkle)
 243                            .selected(default)
 244                            .selected_icon(IconName::SparkleFilled)
 245                            .icon_color(if default { Color::Accent } else { Color::Muted })
 246                            .shape(IconButtonShape::Square)
 247                            .tooltip(move |cx| {
 248                                Tooltip::text(
 249                                    if default {
 250                                        "Remove from Default Prompt"
 251                                    } else {
 252                                        "Add to Default Prompt"
 253                                    },
 254                                    cx,
 255                                )
 256                            })
 257                            .on_click(cx.listener(move |_, _, cx| {
 258                                cx.emit(PromptPickerEvent::ToggledDefault { prompt_id })
 259                            })),
 260                    ),
 261            );
 262        Some(element)
 263    }
 264
 265    fn render_editor(&self, editor: &View<Editor>, cx: &mut ViewContext<Picker<Self>>) -> Div {
 266        h_flex()
 267            .bg(cx.theme().colors().editor_background)
 268            .rounded_md()
 269            .overflow_hidden()
 270            .flex_none()
 271            .py_1()
 272            .px_2()
 273            .mx_1()
 274            .child(editor.clone())
 275    }
 276}
 277
 278impl PromptLibrary {
 279    fn new(
 280        store: Arc<PromptStore>,
 281        language_registry: Arc<LanguageRegistry>,
 282        cx: &mut ViewContext<Self>,
 283    ) -> Self {
 284        let delegate = PromptPickerDelegate {
 285            store: store.clone(),
 286            selected_index: 0,
 287            matches: Vec::new(),
 288        };
 289
 290        let picker = cx.new_view(|cx| {
 291            let picker = Picker::uniform_list(delegate, cx)
 292                .modal(false)
 293                .max_height(None);
 294            picker.focus(cx);
 295            picker
 296        });
 297        Self {
 298            store: store.clone(),
 299            language_registry,
 300            prompt_editors: HashMap::default(),
 301            active_prompt_id: None,
 302            pending_load: Task::ready(()),
 303            _subscriptions: vec![cx.subscribe(&picker, Self::handle_picker_event)],
 304            picker,
 305        }
 306    }
 307
 308    fn handle_picker_event(
 309        &mut self,
 310        _: View<Picker<PromptPickerDelegate>>,
 311        event: &PromptPickerEvent,
 312        cx: &mut ViewContext<Self>,
 313    ) {
 314        match event {
 315            PromptPickerEvent::Selected { prompt_id } => {
 316                self.load_prompt(*prompt_id, false, cx);
 317            }
 318            PromptPickerEvent::Confirmed { prompt_id } => {
 319                self.load_prompt(*prompt_id, true, cx);
 320            }
 321            PromptPickerEvent::ToggledDefault { prompt_id } => {
 322                self.toggle_default_for_prompt(*prompt_id, cx);
 323            }
 324            PromptPickerEvent::Deleted { prompt_id } => {
 325                self.delete_prompt(*prompt_id, cx);
 326            }
 327        }
 328    }
 329
 330    pub fn new_prompt(&mut self, cx: &mut ViewContext<Self>) {
 331        // If we already have an untitled prompt, use that instead
 332        // of creating a new one.
 333        if let Some(metadata) = self.store.first() {
 334            if metadata.title.is_none() {
 335                self.load_prompt(metadata.id, true, cx);
 336                return;
 337            }
 338        }
 339
 340        let prompt_id = PromptId::new();
 341        let save = self.store.save(prompt_id, None, false, "".into());
 342        self.picker.update(cx, |picker, cx| picker.refresh(cx));
 343        cx.spawn(|this, mut cx| async move {
 344            save.await?;
 345            this.update(&mut cx, |this, cx| this.load_prompt(prompt_id, true, cx))
 346        })
 347        .detach_and_log_err(cx);
 348    }
 349
 350    pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
 351        const SAVE_THROTTLE: Duration = Duration::from_millis(500);
 352
 353        let prompt_metadata = self.store.metadata(prompt_id).unwrap();
 354        let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
 355        let title = prompt_editor.title_editor.read(cx).text(cx);
 356        let body = prompt_editor.body_editor.update(cx, |editor, cx| {
 357            editor
 358                .buffer()
 359                .read(cx)
 360                .as_singleton()
 361                .unwrap()
 362                .read(cx)
 363                .as_rope()
 364                .clone()
 365        });
 366
 367        let store = self.store.clone();
 368        let executor = cx.background_executor().clone();
 369
 370        prompt_editor.next_title_and_body_to_save = Some((title, body));
 371        if prompt_editor.pending_save.is_none() {
 372            prompt_editor.pending_save = Some(cx.spawn(|this, mut cx| {
 373                async move {
 374                    loop {
 375                        let title_and_body = this.update(&mut cx, |this, _| {
 376                            this.prompt_editors
 377                                .get_mut(&prompt_id)?
 378                                .next_title_and_body_to_save
 379                                .take()
 380                        })?;
 381
 382                        if let Some((title, body)) = title_and_body {
 383                            let title = if title.trim().is_empty() {
 384                                None
 385                            } else {
 386                                Some(SharedString::from(title))
 387                            };
 388                            store
 389                                .save(prompt_id, title, prompt_metadata.default, body)
 390                                .await
 391                                .log_err();
 392                            this.update(&mut cx, |this, cx| {
 393                                this.picker.update(cx, |picker, cx| picker.refresh(cx));
 394                                cx.notify();
 395                            })?;
 396
 397                            executor.timer(SAVE_THROTTLE).await;
 398                        } else {
 399                            break;
 400                        }
 401                    }
 402
 403                    this.update(&mut cx, |this, _cx| {
 404                        if let Some(prompt_editor) = this.prompt_editors.get_mut(&prompt_id) {
 405                            prompt_editor.pending_save = None;
 406                        }
 407                    })
 408                }
 409                .log_err()
 410            }));
 411        }
 412    }
 413
 414    pub fn delete_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
 415        if let Some(active_prompt_id) = self.active_prompt_id {
 416            self.delete_prompt(active_prompt_id, cx);
 417        }
 418    }
 419
 420    pub fn duplicate_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
 421        if let Some(active_prompt_id) = self.active_prompt_id {
 422            self.duplicate_prompt(active_prompt_id, cx);
 423        }
 424    }
 425
 426    pub fn toggle_default_for_active_prompt(&mut self, cx: &mut ViewContext<Self>) {
 427        if let Some(active_prompt_id) = self.active_prompt_id {
 428            self.toggle_default_for_prompt(active_prompt_id, cx);
 429        }
 430    }
 431
 432    pub fn toggle_default_for_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
 433        if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
 434            self.store
 435                .save_metadata(prompt_id, prompt_metadata.title, !prompt_metadata.default)
 436                .detach_and_log_err(cx);
 437            self.picker.update(cx, |picker, cx| picker.refresh(cx));
 438            cx.notify();
 439        }
 440    }
 441
 442    pub fn load_prompt(&mut self, prompt_id: PromptId, focus: bool, cx: &mut ViewContext<Self>) {
 443        if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
 444            if focus {
 445                prompt_editor
 446                    .body_editor
 447                    .update(cx, |editor, cx| editor.focus(cx));
 448            }
 449            self.set_active_prompt(Some(prompt_id), cx);
 450        } else if let Some(prompt_metadata) = self.store.metadata(prompt_id) {
 451            let language_registry = self.language_registry.clone();
 452            let prompt = self.store.load(prompt_id);
 453            self.pending_load = cx.spawn(|this, mut cx| async move {
 454                let prompt = prompt.await;
 455                let markdown = language_registry.language_for_name("Markdown").await;
 456                this.update(&mut cx, |this, cx| match prompt {
 457                    Ok(prompt) => {
 458                        let title_editor = cx.new_view(|cx| {
 459                            let mut editor = Editor::auto_width(cx);
 460                            editor.set_placeholder_text("Untitled", cx);
 461                            editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
 462                            editor
 463                        });
 464                        let body_editor = cx.new_view(|cx| {
 465                            let buffer = cx.new_model(|cx| {
 466                                let mut buffer = Buffer::local(prompt, cx);
 467                                buffer.set_language(markdown.log_err(), cx);
 468                                buffer.set_language_registry(language_registry);
 469                                buffer
 470                            });
 471
 472                            let mut editor = Editor::for_buffer(buffer, None, cx);
 473                            editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
 474                            editor.set_show_gutter(false, cx);
 475                            editor.set_show_wrap_guides(false, cx);
 476                            editor.set_show_indent_guides(false, cx);
 477                            editor.set_use_modal_editing(false);
 478                            editor.set_current_line_highlight(Some(CurrentLineHighlight::None));
 479                            editor.set_completion_provider(Box::new(
 480                                SlashCommandCompletionProvider::new(None, None),
 481                            ));
 482                            if focus {
 483                                editor.focus(cx);
 484                            }
 485                            editor
 486                        });
 487                        let _subscriptions = vec![
 488                            cx.subscribe(&title_editor, move |this, editor, event, cx| {
 489                                this.handle_prompt_title_editor_event(prompt_id, editor, event, cx)
 490                            }),
 491                            cx.subscribe(&body_editor, move |this, editor, event, cx| {
 492                                this.handle_prompt_body_editor_event(prompt_id, editor, event, cx)
 493                            }),
 494                        ];
 495                        this.prompt_editors.insert(
 496                            prompt_id,
 497                            PromptEditor {
 498                                title_editor,
 499                                body_editor,
 500                                next_title_and_body_to_save: None,
 501                                pending_save: None,
 502                                token_count: None,
 503                                pending_token_count: Task::ready(None),
 504                                _subscriptions,
 505                            },
 506                        );
 507                        this.set_active_prompt(Some(prompt_id), cx);
 508                        this.count_tokens(prompt_id, cx);
 509                    }
 510                    Err(error) => {
 511                        // TODO: we should show the error in the UI.
 512                        log::error!("error while loading prompt: {:?}", error);
 513                    }
 514                })
 515                .ok();
 516            });
 517        }
 518    }
 519
 520    fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
 521        self.active_prompt_id = prompt_id;
 522        self.picker.update(cx, |picker, cx| {
 523            if let Some(prompt_id) = prompt_id {
 524                if picker
 525                    .delegate
 526                    .matches
 527                    .get(picker.delegate.selected_index())
 528                    .map_or(true, |old_selected_prompt| {
 529                        old_selected_prompt.id != prompt_id
 530                    })
 531                {
 532                    if let Some(ix) = picker
 533                        .delegate
 534                        .matches
 535                        .iter()
 536                        .position(|mat| mat.id == prompt_id)
 537                    {
 538                        picker.set_selected_index(ix, true, cx);
 539                    }
 540                }
 541            } else {
 542                picker.focus(cx);
 543            }
 544        });
 545        cx.notify();
 546    }
 547
 548    pub fn delete_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
 549        if let Some(metadata) = self.store.metadata(prompt_id) {
 550            let confirmation = cx.prompt(
 551                PromptLevel::Warning,
 552                &format!(
 553                    "Are you sure you want to delete {}",
 554                    metadata.title.unwrap_or("Untitled".into())
 555                ),
 556                None,
 557                &["Delete", "Cancel"],
 558            );
 559
 560            cx.spawn(|this, mut cx| async move {
 561                if confirmation.await.ok() == Some(0) {
 562                    this.update(&mut cx, |this, cx| {
 563                        if this.active_prompt_id == Some(prompt_id) {
 564                            this.set_active_prompt(None, cx);
 565                        }
 566                        this.prompt_editors.remove(&prompt_id);
 567                        this.store.delete(prompt_id).detach_and_log_err(cx);
 568                        this.picker.update(cx, |picker, cx| picker.refresh(cx));
 569                        cx.notify();
 570                    })?;
 571                }
 572                anyhow::Ok(())
 573            })
 574            .detach_and_log_err(cx);
 575        }
 576    }
 577
 578    pub fn duplicate_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
 579        if let Some(prompt) = self.prompt_editors.get(&prompt_id) {
 580            const DUPLICATE_SUFFIX: &str = " copy";
 581            let title_to_duplicate = prompt.title_editor.read(cx).text(cx);
 582            let existing_titles = self
 583                .prompt_editors
 584                .iter()
 585                .filter(|&(&id, _)| id != prompt_id)
 586                .map(|(_, prompt_editor)| prompt_editor.title_editor.read(cx).text(cx))
 587                .filter(|title| title.starts_with(&title_to_duplicate))
 588                .collect::<HashSet<_>>();
 589
 590            let title = if existing_titles.is_empty() {
 591                title_to_duplicate + DUPLICATE_SUFFIX
 592            } else {
 593                let mut i = 1;
 594                loop {
 595                    let new_title = format!("{title_to_duplicate}{DUPLICATE_SUFFIX} {i}");
 596                    if !existing_titles.contains(&new_title) {
 597                        break new_title;
 598                    }
 599                    i += 1;
 600                }
 601            };
 602
 603            let new_id = PromptId::new();
 604            let body = prompt.body_editor.read(cx).text(cx);
 605            let save = self
 606                .store
 607                .save(new_id, Some(title.into()), false, body.into());
 608            self.picker.update(cx, |picker, cx| picker.refresh(cx));
 609            cx.spawn(|this, mut cx| async move {
 610                save.await?;
 611                this.update(&mut cx, |prompt_library, cx| {
 612                    prompt_library.load_prompt(new_id, true, cx)
 613                })
 614            })
 615            .detach_and_log_err(cx);
 616        }
 617    }
 618
 619    fn focus_active_prompt(&mut self, _: &Tab, cx: &mut ViewContext<Self>) {
 620        if let Some(active_prompt) = self.active_prompt_id {
 621            self.prompt_editors[&active_prompt]
 622                .body_editor
 623                .update(cx, |editor, cx| editor.focus(cx));
 624            cx.stop_propagation();
 625        }
 626    }
 627
 628    fn focus_picker(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
 629        self.picker.update(cx, |picker, cx| picker.focus(cx));
 630    }
 631
 632    pub fn inline_assist(&mut self, action: &InlineAssist, cx: &mut ViewContext<Self>) {
 633        let Some(active_prompt_id) = self.active_prompt_id else {
 634            cx.propagate();
 635            return;
 636        };
 637
 638        let prompt_editor = &self.prompt_editors[&active_prompt_id].body_editor;
 639        let provider = LanguageModelCompletionProvider::read_global(cx);
 640        let initial_prompt = action.prompt.clone();
 641        if provider.is_authenticated(cx) {
 642            InlineAssistant::update_global(cx, |assistant, cx| {
 643                assistant.assist(&prompt_editor, None, None, initial_prompt, cx)
 644            })
 645        } else {
 646            for window in cx.windows() {
 647                if let Some(workspace) = window.downcast::<Workspace>() {
 648                    let panel = workspace
 649                        .update(cx, |workspace, cx| {
 650                            cx.activate_window();
 651                            workspace.focus_panel::<AssistantPanel>(cx)
 652                        })
 653                        .ok()
 654                        .flatten();
 655                    if panel.is_some() {
 656                        return;
 657                    }
 658                }
 659            }
 660        }
 661    }
 662
 663    fn move_down_from_title(&mut self, _: &editor::actions::MoveDown, cx: &mut ViewContext<Self>) {
 664        if let Some(prompt_id) = self.active_prompt_id {
 665            if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
 666                cx.focus_view(&prompt_editor.body_editor);
 667            }
 668        }
 669    }
 670
 671    fn move_up_from_body(&mut self, _: &editor::actions::MoveUp, cx: &mut ViewContext<Self>) {
 672        if let Some(prompt_id) = self.active_prompt_id {
 673            if let Some(prompt_editor) = self.prompt_editors.get(&prompt_id) {
 674                cx.focus_view(&prompt_editor.title_editor);
 675            }
 676        }
 677    }
 678
 679    fn handle_prompt_title_editor_event(
 680        &mut self,
 681        prompt_id: PromptId,
 682        title_editor: View<Editor>,
 683        event: &EditorEvent,
 684        cx: &mut ViewContext<Self>,
 685    ) {
 686        match event {
 687            EditorEvent::BufferEdited => {
 688                self.save_prompt(prompt_id, cx);
 689                self.count_tokens(prompt_id, cx);
 690            }
 691            EditorEvent::Blurred => {
 692                title_editor.update(cx, |title_editor, cx| {
 693                    title_editor.change_selections(None, cx, |selections| {
 694                        let cursor = selections.oldest_anchor().head();
 695                        selections.select_anchor_ranges([cursor..cursor]);
 696                    });
 697                });
 698            }
 699            _ => {}
 700        }
 701    }
 702
 703    fn handle_prompt_body_editor_event(
 704        &mut self,
 705        prompt_id: PromptId,
 706        body_editor: View<Editor>,
 707        event: &EditorEvent,
 708        cx: &mut ViewContext<Self>,
 709    ) {
 710        match event {
 711            EditorEvent::BufferEdited => {
 712                self.save_prompt(prompt_id, cx);
 713                self.count_tokens(prompt_id, cx);
 714            }
 715            EditorEvent::Blurred => {
 716                body_editor.update(cx, |body_editor, cx| {
 717                    body_editor.change_selections(None, cx, |selections| {
 718                        let cursor = selections.oldest_anchor().head();
 719                        selections.select_anchor_ranges([cursor..cursor]);
 720                    });
 721                });
 722            }
 723            _ => {}
 724        }
 725    }
 726
 727    fn count_tokens(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
 728        if let Some(prompt) = self.prompt_editors.get_mut(&prompt_id) {
 729            let editor = &prompt.body_editor.read(cx);
 730            let buffer = &editor.buffer().read(cx).as_singleton().unwrap().read(cx);
 731            let body = buffer.as_rope().clone();
 732            prompt.pending_token_count = cx.spawn(|this, mut cx| {
 733                async move {
 734                    const DEBOUNCE_TIMEOUT: Duration = Duration::from_secs(1);
 735
 736                    cx.background_executor().timer(DEBOUNCE_TIMEOUT).await;
 737                    let token_count = cx
 738                        .update(|cx| {
 739                            LanguageModelCompletionProvider::read_global(cx).count_tokens(
 740                                LanguageModelRequest {
 741                                    messages: vec![LanguageModelRequestMessage {
 742                                        role: Role::System,
 743                                        content: body.to_string(),
 744                                    }],
 745                                    stop: Vec::new(),
 746                                    temperature: 1.,
 747                                },
 748                                cx,
 749                            )
 750                        })?
 751                        .await?;
 752                    this.update(&mut cx, |this, cx| {
 753                        let prompt_editor = this.prompt_editors.get_mut(&prompt_id).unwrap();
 754                        prompt_editor.token_count = Some(token_count);
 755                        cx.notify();
 756                    })
 757                }
 758                .log_err()
 759            });
 760        }
 761    }
 762
 763    fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
 764        v_flex()
 765            .id("prompt-list")
 766            .capture_action(cx.listener(Self::focus_active_prompt))
 767            .bg(cx.theme().colors().panel_background)
 768            .h_full()
 769            .px_1()
 770            .w_1_3()
 771            .overflow_x_hidden()
 772            .child(
 773                h_flex()
 774                    .p(Spacing::Small.rems(cx))
 775                    .h_9()
 776                    .w_full()
 777                    .flex_none()
 778                    .justify_end()
 779                    .child(
 780                        IconButton::new("new-prompt", IconName::Plus)
 781                            .style(ButtonStyle::Transparent)
 782                            .shape(IconButtonShape::Square)
 783                            .tooltip(move |cx| Tooltip::for_action("New Prompt", &NewPrompt, cx))
 784                            .on_click(|_, cx| {
 785                                cx.dispatch_action(Box::new(NewPrompt));
 786                            }),
 787                    ),
 788            )
 789            .child(div().flex_grow().child(self.picker.clone()))
 790    }
 791
 792    fn render_active_prompt(&mut self, cx: &mut ViewContext<PromptLibrary>) -> gpui::Stateful<Div> {
 793        div()
 794            .w_2_3()
 795            .h_full()
 796            .id("prompt-editor")
 797            .border_l_1()
 798            .border_color(cx.theme().colors().border)
 799            .bg(cx.theme().colors().editor_background)
 800            .flex_none()
 801            .min_w_64()
 802            .children(self.active_prompt_id.and_then(|prompt_id| {
 803                let prompt_metadata = self.store.metadata(prompt_id)?;
 804                let prompt_editor = &self.prompt_editors[&prompt_id];
 805                let focus_handle = prompt_editor.body_editor.focus_handle(cx);
 806                let current_model = LanguageModelCompletionProvider::read_global(cx).active_model();
 807                let settings = ThemeSettings::get_global(cx);
 808
 809                Some(
 810                    v_flex()
 811                        .id("prompt-editor-inner")
 812                        .size_full()
 813                        .relative()
 814                        .overflow_hidden()
 815                        .pl(Spacing::XXLarge.rems(cx))
 816                        .pt(Spacing::Large.rems(cx))
 817                        .on_click(cx.listener(move |_, _, cx| {
 818                            cx.focus(&focus_handle);
 819                        }))
 820                        .child(
 821                            h_flex()
 822                                .group("active-editor-header")
 823                                .pr(Spacing::XXLarge.rems(cx))
 824                                .pt(Spacing::XSmall.rems(cx))
 825                                .pb(Spacing::Large.rems(cx))
 826                                .justify_between()
 827                                .child(
 828                                    h_flex().gap_1().child(
 829                                        div()
 830                                            .max_w_80()
 831                                            .on_action(cx.listener(Self::move_down_from_title))
 832                                            .border_1()
 833                                            .border_color(transparent_black())
 834                                            .rounded_md()
 835                                            .group_hover("active-editor-header", |this| {
 836                                                this.border_color(
 837                                                    cx.theme().colors().border_variant,
 838                                                )
 839                                            })
 840                                            .child(EditorElement::new(
 841                                                &prompt_editor.title_editor,
 842                                                EditorStyle {
 843                                                    background: cx.theme().system().transparent,
 844                                                    local_player: cx.theme().players().local(),
 845                                                    text: TextStyle {
 846                                                        color: cx
 847                                                            .theme()
 848                                                            .colors()
 849                                                            .editor_foreground,
 850                                                        font_family: settings
 851                                                            .ui_font
 852                                                            .family
 853                                                            .clone(),
 854                                                        font_features: settings
 855                                                            .ui_font
 856                                                            .features
 857                                                            .clone(),
 858                                                        font_size: HeadlineSize::Large
 859                                                            .size()
 860                                                            .into(),
 861                                                        font_weight: settings.ui_font.weight,
 862                                                        line_height: relative(
 863                                                            settings.buffer_line_height.value(),
 864                                                        ),
 865                                                        ..Default::default()
 866                                                    },
 867                                                    scrollbar_width: Pixels::ZERO,
 868                                                    syntax: cx.theme().syntax().clone(),
 869                                                    status: cx.theme().status().clone(),
 870                                                    inlay_hints_style: HighlightStyle {
 871                                                        color: Some(cx.theme().status().hint),
 872                                                        ..HighlightStyle::default()
 873                                                    },
 874                                                    suggestions_style: HighlightStyle {
 875                                                        color: Some(cx.theme().status().predictive),
 876                                                        ..HighlightStyle::default()
 877                                                    },
 878                                                },
 879                                            )),
 880                                    ),
 881                                )
 882                                .child(
 883                                    h_flex()
 884                                        .h_full()
 885                                        .child(
 886                                            h_flex()
 887                                                .h_full()
 888                                                .gap(Spacing::XXLarge.rems(cx))
 889                                                .child(div()),
 890                                        )
 891                                        .child(
 892                                            h_flex()
 893                                                .h_full()
 894                                                .gap(Spacing::XXLarge.rems(cx))
 895                                                .children(prompt_editor.token_count.map(
 896                                                    |token_count| {
 897                                                        let token_count: SharedString =
 898                                                            token_count.to_string().into();
 899                                                        let label_token_count: SharedString =
 900                                                            token_count.to_string().into();
 901
 902                                                        h_flex()
 903                                                            .id("token_count")
 904                                                            .tooltip(move |cx| {
 905                                                                let token_count =
 906                                                                    token_count.clone();
 907
 908                                                                Tooltip::with_meta(
 909                                                                    format!(
 910                                                                        "{} tokens",
 911                                                                        token_count.clone()
 912                                                                    ),
 913                                                                    None,
 914                                                                    format!(
 915                                                                        "Model: {}",
 916                                                                        current_model
 917                                                                            .as_ref()
 918                                                                            .map(|model| model
 919                                                                                .name()
 920                                                                                .0)
 921                                                                            .unwrap_or_default()
 922                                                                    ),
 923                                                                    cx,
 924                                                                )
 925                                                            })
 926                                                            .child(
 927                                                                Label::new(format!(
 928                                                                    "{} tokens",
 929                                                                    label_token_count.clone()
 930                                                                ))
 931                                                                .color(Color::Muted),
 932                                                            )
 933                                                    },
 934                                                ))
 935                                                .child(
 936                                                    IconButton::new(
 937                                                        "delete-prompt",
 938                                                        IconName::Trash,
 939                                                    )
 940                                                    .size(ButtonSize::Large)
 941                                                    .style(ButtonStyle::Transparent)
 942                                                    .shape(IconButtonShape::Square)
 943                                                    .size(ButtonSize::Large)
 944                                                    .tooltip(move |cx| {
 945                                                        Tooltip::for_action(
 946                                                            "Delete Prompt",
 947                                                            &DeletePrompt,
 948                                                            cx,
 949                                                        )
 950                                                    })
 951                                                    .on_click(|_, cx| {
 952                                                        cx.dispatch_action(Box::new(DeletePrompt));
 953                                                    }),
 954                                                )
 955                                                .child(
 956                                                    IconButton::new(
 957                                                        "duplicate-prompt",
 958                                                        IconName::BookCopy,
 959                                                    )
 960                                                    .size(ButtonSize::Large)
 961                                                    .style(ButtonStyle::Transparent)
 962                                                    .shape(IconButtonShape::Square)
 963                                                    .size(ButtonSize::Large)
 964                                                    .tooltip(move |cx| {
 965                                                        Tooltip::for_action(
 966                                                            "Duplicate Prompt",
 967                                                            &DuplicatePrompt,
 968                                                            cx,
 969                                                        )
 970                                                    })
 971                                                    .on_click(|_, cx| {
 972                                                        cx.dispatch_action(Box::new(
 973                                                            DuplicatePrompt,
 974                                                        ));
 975                                                    }),
 976                                                )
 977                                                .child(
 978                                                    IconButton::new(
 979                                                        "toggle-default-prompt",
 980                                                        IconName::Sparkle,
 981                                                    )
 982                                                    .style(ButtonStyle::Transparent)
 983                                                    .selected(prompt_metadata.default)
 984                                                    .selected_icon(IconName::SparkleFilled)
 985                                                    .icon_color(if prompt_metadata.default {
 986                                                        Color::Accent
 987                                                    } else {
 988                                                        Color::Muted
 989                                                    })
 990                                                    .shape(IconButtonShape::Square)
 991                                                    .size(ButtonSize::Large)
 992                                                    .tooltip(move |cx| {
 993                                                        Tooltip::text(
 994                                                            if prompt_metadata.default {
 995                                                                "Remove from Default Prompt"
 996                                                            } else {
 997                                                                "Add to Default Prompt"
 998                                                            },
 999                                                            cx,
1000                                                        )
1001                                                    })
1002                                                    .on_click(|_, cx| {
1003                                                        cx.dispatch_action(Box::new(
1004                                                            ToggleDefaultPrompt,
1005                                                        ));
1006                                                    }),
1007                                                ),
1008                                        ),
1009                                ),
1010                        )
1011                        .child(
1012                            div()
1013                                .on_action(cx.listener(Self::focus_picker))
1014                                .on_action(cx.listener(Self::inline_assist))
1015                                .on_action(cx.listener(Self::move_up_from_body))
1016                                .flex_grow()
1017                                .h_full()
1018                                .child(prompt_editor.body_editor.clone()),
1019                        ),
1020                )
1021            }))
1022    }
1023}
1024
1025impl Render for PromptLibrary {
1026    fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
1027        let ui_font = theme::setup_ui_font(cx);
1028        let theme = cx.theme().clone();
1029
1030        h_flex()
1031            .id("prompt-manager")
1032            .key_context("PromptLibrary")
1033            .on_action(cx.listener(|this, &NewPrompt, cx| this.new_prompt(cx)))
1034            .on_action(cx.listener(|this, &DeletePrompt, cx| this.delete_active_prompt(cx)))
1035            .on_action(cx.listener(|this, &DuplicatePrompt, cx| this.duplicate_active_prompt(cx)))
1036            .on_action(cx.listener(|this, &ToggleDefaultPrompt, cx| {
1037                this.toggle_default_for_active_prompt(cx)
1038            }))
1039            .size_full()
1040            .overflow_hidden()
1041            .font(ui_font)
1042            .text_color(theme.colors().text)
1043            .child(self.render_prompt_list(cx))
1044            .child(self.render_active_prompt(cx))
1045    }
1046}
1047
1048#[derive(Clone, Debug, Serialize, Deserialize)]
1049pub struct PromptMetadata {
1050    pub id: PromptId,
1051    pub title: Option<SharedString>,
1052    pub default: bool,
1053    pub saved_at: DateTime<Utc>,
1054}
1055
1056#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
1057pub struct PromptId(Uuid);
1058
1059impl PromptId {
1060    pub fn new() -> PromptId {
1061        PromptId(Uuid::new_v4())
1062    }
1063}
1064
1065pub struct PromptStore {
1066    executor: BackgroundExecutor,
1067    env: heed::Env,
1068    bodies: Database<SerdeBincode<PromptId>, SerdeBincode<String>>,
1069    metadata: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
1070    metadata_cache: RwLock<MetadataCache>,
1071}
1072
1073#[derive(Default)]
1074struct MetadataCache {
1075    metadata: Vec<PromptMetadata>,
1076    metadata_by_id: HashMap<PromptId, PromptMetadata>,
1077}
1078
1079impl MetadataCache {
1080    fn from_db(
1081        db: Database<SerdeBincode<PromptId>, SerdeBincode<PromptMetadata>>,
1082        txn: &RoTxn,
1083    ) -> Result<Self> {
1084        let mut cache = MetadataCache::default();
1085        for result in db.iter(txn)? {
1086            let (prompt_id, metadata) = result?;
1087            cache.metadata.push(metadata.clone());
1088            cache.metadata_by_id.insert(prompt_id, metadata);
1089        }
1090        cache.sort();
1091        Ok(cache)
1092    }
1093
1094    fn insert(&mut self, metadata: PromptMetadata) {
1095        self.metadata_by_id.insert(metadata.id, metadata.clone());
1096        if let Some(old_metadata) = self.metadata.iter_mut().find(|m| m.id == metadata.id) {
1097            *old_metadata = metadata;
1098        } else {
1099            self.metadata.push(metadata);
1100        }
1101        self.sort();
1102    }
1103
1104    fn remove(&mut self, id: PromptId) {
1105        self.metadata.retain(|metadata| metadata.id != id);
1106        self.metadata_by_id.remove(&id);
1107    }
1108
1109    fn sort(&mut self) {
1110        self.metadata.sort_unstable_by(|a, b| {
1111            a.title
1112                .cmp(&b.title)
1113                .then_with(|| b.saved_at.cmp(&a.saved_at))
1114        });
1115    }
1116}
1117
1118impl PromptStore {
1119    pub fn global(cx: &AppContext) -> impl Future<Output = Result<Arc<Self>>> {
1120        let store = GlobalPromptStore::global(cx).0.clone();
1121        async move { store.await.map_err(|err| anyhow!(err)) }
1122    }
1123
1124    pub fn new(db_path: PathBuf, executor: BackgroundExecutor) -> Task<Result<Self>> {
1125        executor.spawn({
1126            let executor = executor.clone();
1127            async move {
1128                std::fs::create_dir_all(&db_path)?;
1129
1130                let db_env = unsafe {
1131                    heed::EnvOpenOptions::new()
1132                        .map_size(1024 * 1024 * 1024) // 1GB
1133                        .max_dbs(2) // bodies and metadata
1134                        .open(db_path)?
1135                };
1136
1137                let mut txn = db_env.write_txn()?;
1138                let bodies = db_env.create_database(&mut txn, Some("bodies"))?;
1139                let metadata = db_env.create_database(&mut txn, Some("metadata"))?;
1140                let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
1141                txn.commit()?;
1142
1143                Ok(PromptStore {
1144                    executor,
1145                    env: db_env,
1146                    bodies,
1147                    metadata,
1148                    metadata_cache: RwLock::new(metadata_cache),
1149                })
1150            }
1151        })
1152    }
1153
1154    pub fn load(&self, id: PromptId) -> Task<Result<String>> {
1155        let env = self.env.clone();
1156        let bodies = self.bodies;
1157        self.executor.spawn(async move {
1158            let txn = env.read_txn()?;
1159            bodies
1160                .get(&txn, &id)?
1161                .ok_or_else(|| anyhow!("prompt not found"))
1162        })
1163    }
1164
1165    pub fn default_prompt_metadata(&self) -> Vec<PromptMetadata> {
1166        return self
1167            .metadata_cache
1168            .read()
1169            .metadata
1170            .iter()
1171            .filter(|metadata| metadata.default)
1172            .cloned()
1173            .collect::<Vec<_>>();
1174    }
1175
1176    pub fn delete(&self, id: PromptId) -> Task<Result<()>> {
1177        self.metadata_cache.write().remove(id);
1178
1179        let db_connection = self.env.clone();
1180        let bodies = self.bodies;
1181        let metadata = self.metadata;
1182
1183        self.executor.spawn(async move {
1184            let mut txn = db_connection.write_txn()?;
1185
1186            metadata.delete(&mut txn, &id)?;
1187            bodies.delete(&mut txn, &id)?;
1188
1189            txn.commit()?;
1190            Ok(())
1191        })
1192    }
1193
1194    fn metadata(&self, id: PromptId) -> Option<PromptMetadata> {
1195        self.metadata_cache.read().metadata_by_id.get(&id).cloned()
1196    }
1197
1198    pub fn id_for_title(&self, title: &str) -> Option<PromptId> {
1199        let metadata_cache = self.metadata_cache.read();
1200        let metadata = metadata_cache
1201            .metadata
1202            .iter()
1203            .find(|metadata| metadata.title.as_ref().map(|title| &***title) == Some(title))?;
1204        Some(metadata.id)
1205    }
1206
1207    pub fn search(&self, query: String) -> Task<Vec<PromptMetadata>> {
1208        let cached_metadata = self.metadata_cache.read().metadata.clone();
1209        let executor = self.executor.clone();
1210        self.executor.spawn(async move {
1211            let mut matches = if query.is_empty() {
1212                cached_metadata
1213            } else {
1214                let candidates = cached_metadata
1215                    .iter()
1216                    .enumerate()
1217                    .filter_map(|(ix, metadata)| {
1218                        Some(StringMatchCandidate::new(
1219                            ix,
1220                            metadata.title.as_ref()?.to_string(),
1221                        ))
1222                    })
1223                    .collect::<Vec<_>>();
1224                let matches = fuzzy::match_strings(
1225                    &candidates,
1226                    &query,
1227                    false,
1228                    100,
1229                    &AtomicBool::default(),
1230                    executor,
1231                )
1232                .await;
1233                matches
1234                    .into_iter()
1235                    .map(|mat| cached_metadata[mat.candidate_id].clone())
1236                    .collect()
1237            };
1238            matches.sort_by_key(|metadata| Reverse(metadata.default));
1239            matches
1240        })
1241    }
1242
1243    fn save(
1244        &self,
1245        id: PromptId,
1246        title: Option<SharedString>,
1247        default: bool,
1248        body: Rope,
1249    ) -> Task<Result<()>> {
1250        let prompt_metadata = PromptMetadata {
1251            id,
1252            title,
1253            default,
1254            saved_at: Utc::now(),
1255        };
1256        self.metadata_cache.write().insert(prompt_metadata.clone());
1257
1258        let db_connection = self.env.clone();
1259        let bodies = self.bodies;
1260        let metadata = self.metadata;
1261
1262        self.executor.spawn(async move {
1263            let mut txn = db_connection.write_txn()?;
1264
1265            metadata.put(&mut txn, &id, &prompt_metadata)?;
1266            bodies.put(&mut txn, &id, &body.to_string())?;
1267
1268            txn.commit()?;
1269
1270            Ok(())
1271        })
1272    }
1273
1274    fn save_metadata(
1275        &self,
1276        id: PromptId,
1277        title: Option<SharedString>,
1278        default: bool,
1279    ) -> Task<Result<()>> {
1280        let prompt_metadata = PromptMetadata {
1281            id,
1282            title,
1283            default,
1284            saved_at: Utc::now(),
1285        };
1286        self.metadata_cache.write().insert(prompt_metadata.clone());
1287
1288        let db_connection = self.env.clone();
1289        let metadata = self.metadata;
1290
1291        self.executor.spawn(async move {
1292            let mut txn = db_connection.write_txn()?;
1293            metadata.put(&mut txn, &id, &prompt_metadata)?;
1294            txn.commit()?;
1295
1296            Ok(())
1297        })
1298    }
1299
1300    fn first(&self) -> Option<PromptMetadata> {
1301        self.metadata_cache.read().metadata.first().cloned()
1302    }
1303
1304    pub fn operations_prompt(&self) -> String {
1305        String::from_utf8(
1306            Assets
1307                .load("prompts/operations.md")
1308                .unwrap()
1309                .unwrap()
1310                .to_vec(),
1311        )
1312        .unwrap()
1313    }
1314}
1315
1316/// Wraps a shared future to a prompt store so it can be assigned as a context global.
1317pub struct GlobalPromptStore(
1318    Shared<BoxFuture<'static, Result<Arc<PromptStore>, Arc<anyhow::Error>>>>,
1319);
1320
1321impl Global for GlobalPromptStore {}