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