prompt_library.rs

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