prompt_library.rs

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