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