prompt_library.rs

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