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