prompt_library.rs

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