prompt_library.rs

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