prompt_library.rs

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