prompt_library.rs

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