prompt_library.rs

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