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