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