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