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