prompt_library.rs

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