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