prompt_library.rs

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