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