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