prompt_library.rs

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