Polish prompt library UX (#12647)

This could still use some improvement UI-wise but the user experience
should be a lot better.

- [x] Show in "Window" application menu
- [x] Load prompt as it's selected in the picker
- [x] Refocus picker on `esc`
- [x] When creating a new prompt, if a new prompt already exists and is
unedited, activate it instead
- [x] Add `/default` command
- [x] Evaluate /commands on prompt insertion
- [x] Autocomplete /commands (but don't evaluate) during prompt editing
- [x] Show token count using the settings model, right-aligned in the
editor
- [x] Picker 
- [x] Sorted alpha
- [x] 2 sublists
    - Default
        - Empty state: Star a prompt to add it to your default prompt
        - Otherwise show prompts with star on hover
    - All
        - Move prompts with star on hover

Release Notes:

- N/A
This commit is contained in:
Antonio Scandurra 2024-06-04 18:36:54 +02:00 committed by GitHub
parent e4bb666eab
commit c5b22eee2d
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
22 changed files with 716 additions and 358 deletions

View file

@ -96,6 +96,7 @@ impl SlashCommand for ActiveSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
});

View file

@ -0,0 +1,81 @@
use super::{prompt_command::PromptPlaceholder, SlashCommand, SlashCommandOutput};
use crate::prompt_library::PromptStore;
use anyhow::{anyhow, Result};
use assistant_slash_command::SlashCommandOutputSection;
use gpui::{AppContext, Task, WeakView};
use language::LspAdapterDelegate;
use std::{
fmt::Write,
sync::{atomic::AtomicBool, Arc},
};
use ui::prelude::*;
use workspace::Workspace;
pub(crate) struct DefaultSlashCommand;
impl SlashCommand for DefaultSlashCommand {
fn name(&self) -> String {
"default".into()
}
fn description(&self) -> String {
"insert default prompt".into()
}
fn menu_text(&self) -> String {
"Insert Default Prompt".into()
}
fn requires_argument(&self) -> bool {
false
}
fn complete_argument(
&self,
_query: String,
_cancellation_flag: Arc<AtomicBool>,
_workspace: WeakView<Workspace>,
_cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
Task::ready(Err(anyhow!("this command does not require argument")))
}
fn run(
self: Arc<Self>,
_argument: Option<&str>,
_workspace: WeakView<Workspace>,
_delegate: Arc<dyn LspAdapterDelegate>,
cx: &mut WindowContext,
) -> Task<Result<SlashCommandOutput>> {
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {
let store = store.await?;
let prompts = store.default_prompt_metadata();
let mut text = String::new();
writeln!(text, "Default Prompt:").unwrap();
for prompt in prompts {
if let Some(title) = prompt.title {
writeln!(text, "/prompt {}", title).unwrap();
}
}
text.pop();
Ok(SlashCommandOutput {
sections: vec![SlashCommandOutputSection {
range: 0..text.len(),
render_placeholder: Arc::new(move |id, unfold, _cx| {
PromptPlaceholder {
title: "Default".into(),
id,
unfold,
}
.into_any_element()
}),
}],
text,
run_commands_in_text: true,
})
})
}
}

View file

@ -107,6 +107,7 @@ impl SlashCommand for FetchSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}

View file

@ -187,6 +187,7 @@ impl SlashCommand for FileSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}

View file

@ -148,6 +148,7 @@ impl SlashCommand for ProjectSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
});

View file

@ -8,15 +8,7 @@ use std::sync::{atomic::AtomicBool, Arc};
use ui::{prelude::*, ButtonLike, ElevationIndex};
use workspace::Workspace;
pub(crate) struct PromptSlashCommand {
store: Arc<PromptStore>,
}
impl PromptSlashCommand {
pub fn new(store: Arc<PromptStore>) -> Self {
Self { store }
}
}
pub(crate) struct PromptSlashCommand;
impl SlashCommand for PromptSlashCommand {
fn name(&self) -> String {
@ -42,9 +34,9 @@ impl SlashCommand for PromptSlashCommand {
_workspace: WeakView<Workspace>,
cx: &mut AppContext,
) -> Task<Result<Vec<String>>> {
let store = self.store.clone();
let store = PromptStore::global(cx);
cx.background_executor().spawn(async move {
let prompts = store.search(query).await;
let prompts = store.await?.search(query).await;
Ok(prompts
.into_iter()
.filter_map(|prompt| Some(prompt.title?.to_string()))
@ -63,11 +55,12 @@ impl SlashCommand for PromptSlashCommand {
return Task::ready(Err(anyhow!("missing prompt name")));
};
let store = self.store.clone();
let store = PromptStore::global(cx);
let title = SharedString::from(title.to_string());
let prompt = cx.background_executor().spawn({
let title = title.clone();
async move {
let store = store.await?;
let prompt_id = store
.id_for_title(&title)
.with_context(|| format!("no prompt found with title {:?}", title))?;
@ -91,6 +84,7 @@ impl SlashCommand for PromptSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: true,
})
})
}

View file

@ -192,6 +192,7 @@ impl SlashCommand for RustdocSlashCommand {
.into_any_element()
}),
}],
run_commands_in_text: false,
})
})
}

View file

@ -181,7 +181,11 @@ impl SlashCommand for SearchSlashCommand {
}),
});
SlashCommandOutput { text, sections }
SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
}
})
.await;

View file

@ -109,7 +109,11 @@ impl SlashCommand for TabsSlashCommand {
});
}
Ok(SlashCommandOutput { text, sections })
Ok(SlashCommandOutput {
text,
sections,
run_commands_in_text: false,
})
}),
Err(error) => Task::ready(Err(error)),
}