Prompt library updates (#11988)

Restructure prompts & the prompt library.

- Prompts are now written in markdown
- The prompt manager has a picker and editable prompts
- Saving isn't wired up yet
- This also removes the "Insert active prompt" button as this concept doesn't exist anymore, and will be replaced with slash commands.

I didn't staff flag this, but if you do play around with it expect it to still be pretty rough.

Release Notes:

- N/A

---------

Co-authored-by: Nathan Sobo <1789+nathansobo@users.noreply.github.com>
Co-authored-by: Antonio Scandurra <me@as-cii.com>
This commit is contained in:
Nate Butler 2024-05-22 18:04:47 -04:00 committed by GitHub
parent a73a3ef243
commit 0a848f29e8
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
14 changed files with 964 additions and 679 deletions

View file

@ -50,6 +50,8 @@ ui.workspace = true
util.workspace = true
uuid.workspace = true
workspace.workspace = true
picker.workspace = true
gray_matter = "0.2.7"
[dev-dependencies]
ctor.workspace = true

View file

@ -3,7 +3,6 @@ pub mod assistant_panel;
pub mod assistant_settings;
mod codegen;
mod completion_provider;
mod prompt_library;
mod prompts;
mod saved_conversation;
mod search;
@ -17,7 +16,7 @@ use client::{proto, Client};
use command_palette_hooks::CommandPaletteFilter;
pub(crate) use completion_provider::*;
use gpui::{actions, AppContext, Global, SharedString, UpdateGlobal};
pub(crate) use prompt_library::*;
pub(crate) use prompts::prompt_library::*;
pub(crate) use saved_conversation::*;
use serde::{Deserialize, Serialize};
use settings::{Settings, SettingsStore};

View file

@ -1,17 +1,19 @@
use crate::ambient_context::{AmbientContext, ContextUpdated, RecentBuffer};
use crate::prompts::prompt_library::PromptLibrary;
use crate::prompts::prompt_manager::PromptManager;
use crate::{
ambient_context::*,
assistant_settings::{AssistantDockPosition, AssistantSettings, ZedDotDevModel},
codegen::{self, Codegen, CodegenKind},
prompt_library::{PromptLibrary, PromptManager},
prompts::generate_content_prompt,
prompts::prompt::generate_content_prompt,
search::*,
slash_command::{
SlashCommandCleanup, SlashCommandCompletionProvider, SlashCommandLine, SlashCommandRegistry,
},
ApplyEdit, Assist, CompletionProvider, CycleMessageRole, InlineAssist, InsertActivePrompt,
LanguageModel, LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata,
MessageStatus, QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata,
SavedMessage, Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation,
ApplyEdit, Assist, CompletionProvider, CycleMessageRole, InlineAssist, LanguageModel,
LanguageModelRequest, LanguageModelRequestMessage, MessageId, MessageMetadata, MessageStatus,
QuoteSelection, ResetKey, Role, SavedConversation, SavedConversationMetadata, SavedMessage,
Split, ToggleFocus, ToggleHistory, ToggleIncludeConversation,
};
use anyhow::{anyhow, Result};
use client::telemetry::Telemetry;
@ -85,7 +87,7 @@ pub fn init(cx: &mut AppContext) {
})
.register_action(AssistantPanel::inline_assist)
.register_action(AssistantPanel::cancel_last_inline_assist)
.register_action(ConversationEditor::insert_active_prompt)
// .register_action(ConversationEditor::insert_active_prompt)
.register_action(ConversationEditor::quote_selection);
},
)
@ -139,7 +141,7 @@ impl AssistantPanel {
.unwrap_or_default();
let prompt_library = Arc::new(
PromptLibrary::init(fs.clone())
PromptLibrary::load(fs.clone())
.await
.log_err()
.unwrap_or_default(),
@ -1035,20 +1037,20 @@ impl AssistantPanel {
.ok();
}
})
.entry("Insert Active Prompt", None, {
let workspace = workspace.clone();
move |cx| {
workspace
.update(cx, |workspace, cx| {
ConversationEditor::insert_active_prompt(
workspace,
&Default::default(),
cx,
)
})
.ok();
}
})
// .entry("Insert Active Prompt", None, {
// let workspace = workspace.clone();
// move |cx| {
// workspace
// .update(cx, |workspace, cx| {
// ConversationEditor::insert_active_prompt(
// workspace,
// &Default::default(),
// cx,
// )
// })
// .ok();
// }
// })
})
.into()
})
@ -1132,7 +1134,14 @@ impl AssistantPanel {
fn show_prompt_manager(&mut self, cx: &mut ViewContext<Self>) {
if let Some(workspace) = self.workspace.upgrade() {
workspace.update(cx, |workspace, cx| {
workspace.toggle_modal(cx, |cx| PromptManager::new(self.prompt_library.clone(), cx))
workspace.toggle_modal(cx, |cx| {
PromptManager::new(
self.prompt_library.clone(),
self.languages.clone(),
self.fs.clone(),
cx,
)
})
})
}
}
@ -3252,35 +3261,35 @@ impl ConversationEditor {
}
}
fn insert_active_prompt(
workspace: &mut Workspace,
_: &InsertActivePrompt,
cx: &mut ViewContext<Workspace>,
) {
let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
return;
};
// fn insert_active_prompt(
// workspace: &mut Workspace,
// _: &InsertActivePrompt,
// cx: &mut ViewContext<Workspace>,
// ) {
// let Some(panel) = workspace.panel::<AssistantPanel>(cx) else {
// return;
// };
if !panel.focus_handle(cx).contains_focused(cx) {
workspace.toggle_panel_focus::<AssistantPanel>(cx);
}
// if !panel.focus_handle(cx).contains_focused(cx) {
// workspace.toggle_panel_focus::<AssistantPanel>(cx);
// }
if let Some(default_prompt) = panel.read(cx).prompt_library.clone().default_prompt() {
panel.update(cx, |panel, cx| {
if let Some(conversation) = panel
.active_conversation_editor()
.cloned()
.or_else(|| panel.new_conversation(cx))
{
conversation.update(cx, |conversation, cx| {
conversation
.editor
.update(cx, |editor, cx| editor.insert(&default_prompt, cx))
});
};
});
};
}
// if let Some(default_prompt) = panel.read(cx).prompt_library.clone().default_prompt() {
// panel.update(cx, |panel, cx| {
// if let Some(conversation) = panel
// .active_conversation_editor()
// .cloned()
// .or_else(|| panel.new_conversation(cx))
// {
// conversation.update(cx, |conversation, cx| {
// conversation
// .editor
// .update(cx, |editor, cx| editor.insert(&default_prompt, cx))
// });
// };
// });
// };
// }
fn copy(&mut self, _: &editor::actions::Copy, cx: &mut ViewContext<Self>) {
let editor = self.editor.read(cx);

View file

@ -1,454 +0,0 @@
use fs::Fs;
use futures::StreamExt;
use gpui::{AppContext, DismissEvent, EventEmitter, FocusHandle, FocusableView, Render};
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use std::collections::HashMap;
use std::sync::Arc;
use ui::{prelude::*, Checkbox, ModalHeader};
use util::{paths::PROMPTS_DIR, ResultExt};
use workspace::ModalView;
pub struct PromptLibraryState {
/// The default prompt all assistant contexts will start with
_system_prompt: String,
/// All [UserPrompt]s loaded into the library
prompts: HashMap<String, UserPrompt>,
/// Prompts included in the default prompt
default_prompts: Vec<String>,
/// Prompts that have a pending update that hasn't been applied yet
_updateable_prompts: Vec<String>,
/// Prompts that have been changed since they were loaded
/// and can be reverted to their original state
_revertable_prompts: Vec<String>,
version: usize,
}
pub struct PromptLibrary {
state: RwLock<PromptLibraryState>,
}
impl Default for PromptLibrary {
fn default() -> Self {
Self::new()
}
}
impl PromptLibrary {
fn new() -> Self {
Self {
state: RwLock::new(PromptLibraryState {
_system_prompt: String::new(),
prompts: HashMap::new(),
default_prompts: Vec::new(),
_updateable_prompts: Vec::new(),
_revertable_prompts: Vec::new(),
version: 0,
}),
}
}
pub async fn init(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
let prompt_library = PromptLibrary::new();
prompt_library.load_prompts(fs)?;
Ok(prompt_library)
}
fn load_prompts(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
let prompts = futures::executor::block_on(UserPrompt::list(fs))?;
let prompts_with_ids = prompts
.clone()
.into_iter()
.map(|prompt| {
let id = uuid::Uuid::new_v4().to_string();
(id, prompt)
})
.collect::<Vec<_>>();
let mut state = self.state.write();
state.prompts.extend(prompts_with_ids);
state.version += 1;
Ok(())
}
pub fn default_prompt(&self) -> Option<String> {
let state = self.state.read();
if state.default_prompts.is_empty() {
None
} else {
Some(self.join_default_prompts())
}
}
pub fn add_prompt_to_default(&self, prompt_id: String) -> anyhow::Result<()> {
let mut state = self.state.write();
if !state.default_prompts.contains(&prompt_id) && state.prompts.contains_key(&prompt_id) {
state.default_prompts.push(prompt_id);
state.version += 1;
}
Ok(())
}
pub fn remove_prompt_from_default(&self, prompt_id: String) -> anyhow::Result<()> {
let mut state = self.state.write();
state.default_prompts.retain(|id| id != &prompt_id);
state.version += 1;
Ok(())
}
fn join_default_prompts(&self) -> String {
let state = self.state.read();
let active_prompt_ids = state.default_prompts.to_vec();
active_prompt_ids
.iter()
.filter_map(|id| state.prompts.get(id).map(|p| p.prompt.clone()))
.collect::<Vec<_>>()
.join("\n\n---\n\n")
}
#[allow(unused)]
pub fn prompts(&self) -> Vec<UserPrompt> {
let state = self.state.read();
state.prompts.values().cloned().collect()
}
pub fn prompts_with_ids(&self) -> Vec<(String, UserPrompt)> {
let state = self.state.read();
state
.prompts
.iter()
.map(|(id, prompt)| (id.clone(), prompt.clone()))
.collect()
}
pub fn _default_prompts(&self) -> Vec<UserPrompt> {
let state = self.state.read();
state
.default_prompts
.iter()
.filter_map(|id| state.prompts.get(id).cloned())
.collect()
}
pub fn default_prompt_ids(&self) -> Vec<String> {
let state = self.state.read();
state.default_prompts.clone()
}
}
/// A custom prompt that can be loaded into the prompt library
///
/// Example:
///
/// ```json
/// {
/// "title": "Foo",
/// "version": "1.0",
/// "author": "Jane Kim <jane@kim.com>",
/// "languages": ["*"], // or ["rust", "python", "javascript"] etc...
/// "prompt": "bar"
/// }
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct UserPrompt {
version: String,
pub title: String,
author: String,
languages: Vec<String>,
pub prompt: String,
}
impl UserPrompt {
async fn list(fs: Arc<dyn Fs>) -> anyhow::Result<Vec<Self>> {
fs.create_dir(&PROMPTS_DIR).await?;
let mut paths = fs.read_dir(&PROMPTS_DIR).await?;
let mut prompts = Vec::new();
while let Some(path_result) = paths.next().await {
let path = match path_result {
Ok(p) => p,
Err(e) => {
eprintln!("Error reading path: {:?}", e);
continue;
}
};
if path.extension() == Some(std::ffi::OsStr::new("json")) {
match fs.load(&path).await {
Ok(content) => {
let user_prompt: UserPrompt =
serde_json::from_str(&content).map_err(|e| {
anyhow::anyhow!("Failed to deserialize UserPrompt: {}", e)
})?;
prompts.push(user_prompt);
}
Err(e) => eprintln!("Failed to load file {}: {}", path.display(), e),
}
}
}
Ok(prompts)
}
}
pub struct PromptManager {
focus_handle: FocusHandle,
prompt_library: Arc<PromptLibrary>,
active_prompt: Option<String>,
}
impl PromptManager {
pub fn new(prompt_library: Arc<PromptLibrary>, cx: &mut WindowContext) -> Self {
let focus_handle = cx.focus_handle();
Self {
focus_handle,
prompt_library,
active_prompt: None,
}
}
pub fn set_active_prompt(&mut self, prompt_id: Option<String>) {
self.active_prompt = prompt_id;
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
}
impl Render for PromptManager {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let prompt_library = self.prompt_library.clone();
let prompts = prompt_library
.clone()
.prompts_with_ids()
.clone()
.into_iter()
.collect::<Vec<_>>();
let active_prompt = self.active_prompt.as_ref().and_then(|id| {
prompt_library
.prompts_with_ids()
.iter()
.find(|(prompt_id, _)| prompt_id == id)
.map(|(_, prompt)| prompt.clone())
});
v_flex()
.key_context("PromptManager")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
.elevation_3(cx)
.size_full()
.flex_none()
.w(rems(54.))
.h(rems(40.))
.overflow_hidden()
.child(
ModalHeader::new()
.headline("Prompt Library")
.show_dismiss_button(true),
)
.child(
h_flex()
.flex_grow()
.overflow_hidden()
.border_t_1()
.border_color(cx.theme().colors().border)
.child(
div()
.id("prompt-preview")
.overflow_y_scroll()
.h_full()
.min_w_64()
.max_w_1_2()
.child(
v_flex()
.justify_start()
.py(Spacing::Medium.rems(cx))
.px(Spacing::Large.rems(cx))
.bg(cx.theme().colors().surface_background)
.when_else(
!prompts.is_empty(),
|with_items| {
with_items.children(prompts.into_iter().map(
|(id, prompt)| {
let prompt_library = prompt_library.clone();
let prompt = prompt.clone();
let prompt_id = id.clone();
let shared_string_id: SharedString =
id.clone().into();
let default_prompt_ids =
prompt_library.clone().default_prompt_ids();
let is_default =
default_prompt_ids.contains(&id);
// We'll use this for conditionally enabled prompts
// like those loaded only for certain languages
let is_conditional = false;
let selection =
match (is_default, is_conditional) {
(_, true) => Selection::Indeterminate,
(true, _) => Selection::Selected,
(false, _) => Selection::Unselected,
};
v_flex()
.id(ElementId::Name(
format!("prompt-{}", shared_string_id)
.into(),
))
.p(Spacing::Small.rems(cx))
.on_click(cx.listener({
let prompt_id = prompt_id.clone();
move |this, _event, _cx| {
this.set_active_prompt(Some(
prompt_id.clone(),
));
}
}))
.child(
h_flex()
.justify_between()
.child(
h_flex()
.gap(Spacing::Large.rems(cx))
.child(
Checkbox::new(
shared_string_id,
selection,
)
.on_click(move |_, _cx| {
if is_default {
prompt_library
.clone()
.remove_prompt_from_default(
prompt_id.clone(),
)
.log_err();
} else {
prompt_library
.clone()
.add_prompt_to_default(
prompt_id.clone(),
)
.log_err();
}
}),
)
.child(Label::new(
prompt.title,
)),
)
.child(div()),
)
},
))
},
|no_items| {
no_items.child(
Label::new("No prompts").color(Color::Placeholder),
)
},
),
),
)
.child(
div()
.id("prompt-preview")
.overflow_y_scroll()
.border_l_1()
.border_color(cx.theme().colors().border)
.size_full()
.flex_none()
.child(
v_flex()
.justify_start()
.py(Spacing::Medium.rems(cx))
.px(Spacing::Large.rems(cx))
.gap(Spacing::Large.rems(cx))
.when_else(
active_prompt.is_some(),
|with_prompt| {
let active_prompt = active_prompt.as_ref().unwrap();
with_prompt
.child(
v_flex()
.gap_0p5()
.child(
Headline::new(
active_prompt.title.clone(),
)
.size(HeadlineSize::XSmall),
)
.child(
h_flex()
.child(
Label::new(
active_prompt
.author
.clone(),
)
.size(LabelSize::XSmall)
.color(Color::Muted),
)
.child(
Label::new(
if active_prompt
.languages
.is_empty()
|| active_prompt
.languages[0]
== "*"
{
" · Global".to_string()
} else {
format!(
" · {}",
active_prompt
.languages
.join(", ")
)
},
)
.size(LabelSize::XSmall)
.color(Color::Muted),
),
),
)
.child(
div()
.w_full()
.max_w(rems(30.))
.text_ui(cx)
.child(active_prompt.prompt.clone()),
)
},
|without_prompt| {
without_prompt.justify_center().items_center().child(
Label::new("Select a prompt to view details.")
.color(Color::Placeholder),
)
},
),
),
),
)
}
}
impl EventEmitter<DismissEvent> for PromptManager {}
impl ModalView for PromptManager {}
impl FocusableView for PromptManager {
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}

View file

@ -1,95 +1,3 @@
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}
pub mod prompt;
pub mod prompt_library;
pub mod prompt_manager;

View file

@ -0,0 +1,278 @@
use language::BufferSnapshot;
use std::{fmt::Write, ops::Range};
use ui::SharedString;
use gray_matter::{engine::YAML, Matter};
use serde::{Deserialize, Serialize};
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPromptFrontmatter {
title: String,
version: String,
author: String,
#[serde(default)]
languages: Vec<String>,
#[serde(default)]
dependencies: Vec<String>,
}
impl Default for StaticPromptFrontmatter {
fn default() -> Self {
Self {
title: "New Prompt".to_string(),
version: "1.0".to_string(),
author: "No Author".to_string(),
languages: vec!["*".to_string()],
dependencies: vec![],
}
}
}
impl StaticPromptFrontmatter {
pub fn title(&self) -> SharedString {
self.title.clone().into()
}
// pub fn version(&self) -> SharedString {
// self.version.clone().into()
// }
// pub fn author(&self) -> SharedString {
// self.author.clone().into()
// }
// pub fn languages(&self) -> Vec<SharedString> {
// self.languages
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.dependencies
// .clone()
// .into_iter()
// .map(|s| s.into())
// .collect()
// }
}
/// A statuc prompt that can be loaded into the prompt library
/// from Markdown with a frontmatter header
///
/// Examples:
///
/// ### Globally available prompt
///
/// ```markdown
/// ---
/// title: Foo
/// version: 1.0
/// author: Jane Kim <jane@kim.com
/// languages: ["*"]
/// dependencies: []
/// ---
///
/// Foo and bar are terms used in programming to describe generic concepts.
/// ```
///
/// ### Language-specific prompt
///
/// ```markdown
/// ---
/// title: UI with GPUI
/// version: 1.0
/// author: Nate Butler <iamnbutler@gmail.com>
/// languages: ["rust"]
/// dependencies: ["gpui"]
/// ---
///
/// When building a UI with GPUI, ensure you...
/// ```
#[derive(Serialize, Deserialize, Debug, Clone, PartialEq)]
pub struct StaticPrompt {
content: String,
file_name: Option<String>,
}
impl StaticPrompt {
pub fn new(content: String) -> Self {
StaticPrompt {
content,
file_name: None,
}
}
pub fn title(&self) -> Option<SharedString> {
self.metadata().map(|m| m.title())
}
// pub fn version(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.version())
// }
// pub fn author(&self) -> Option<SharedString> {
// self.metadata().map(|m| m.author())
// }
// pub fn languages(&self) -> Vec<SharedString> {
// self.metadata().map(|m| m.languages()).unwrap_or_default()
// }
// pub fn dependencies(&self) -> Vec<SharedString> {
// self.metadata()
// .map(|m| m.dependencies())
// .unwrap_or_default()
// }
// pub fn load(fs: Arc<Fs>, file_name: String) -> anyhow::Result<Self> {
// todo!()
// }
// pub fn save(&self, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
// pub fn rename(&self, new_file_name: String, fs: Arc<Fs>) -> anyhow::Result<()> {
// todo!()
// }
}
impl StaticPrompt {
// pub fn update(&mut self, contents: String) -> &mut Self {
// self.content = contents;
// self
// }
/// Sets the file name of the prompt
pub fn file_name(&mut self, file_name: String) -> &mut Self {
self.file_name = Some(file_name);
self
}
/// Sets the file name of the prompt based on the title
// pub fn file_name_from_title(&mut self) -> &mut Self {
// if let Some(title) = self.title() {
// let file_name = title.to_lowercase().replace(" ", "_");
// if !file_name.is_empty() {
// self.file_name = Some(file_name);
// }
// }
// self
// }
/// Returns the prompt's content
pub fn content(&self) -> &String {
&self.content
}
fn parse(&self) -> anyhow::Result<(StaticPromptFrontmatter, String)> {
let matter = Matter::<YAML>::new();
let result = matter.parse(self.content.as_str());
match result.data {
Some(data) => {
let front_matter: StaticPromptFrontmatter = data.deserialize()?;
let body = result.content;
Ok((front_matter, body))
}
None => Err(anyhow::anyhow!("Failed to parse frontmatter")),
}
}
pub fn metadata(&self) -> Option<StaticPromptFrontmatter> {
self.parse().ok().map(|(front_matter, _)| front_matter)
}
}
pub fn generate_content_prompt(
user_prompt: String,
language_name: Option<&str>,
buffer: BufferSnapshot,
range: Range<usize>,
project_name: Option<String>,
) -> anyhow::Result<String> {
let mut prompt = String::new();
let content_type = match language_name {
None | Some("Markdown" | "Plain Text") => {
writeln!(prompt, "You are an expert engineer.")?;
"Text"
}
Some(language_name) => {
writeln!(prompt, "You are an expert {language_name} engineer.")?;
writeln!(
prompt,
"Your answer MUST always and only be valid {}.",
language_name
)?;
"Code"
}
};
if let Some(project_name) = project_name {
writeln!(
prompt,
"You are currently working inside the '{project_name}' project in code editor Zed."
)?;
}
// Include file content.
for chunk in buffer.text_for_range(0..range.start) {
prompt.push_str(chunk);
}
if range.is_empty() {
prompt.push_str("<|START|>");
} else {
prompt.push_str("<|START|");
}
for chunk in buffer.text_for_range(range.clone()) {
prompt.push_str(chunk);
}
if !range.is_empty() {
prompt.push_str("|END|>");
}
for chunk in buffer.text_for_range(range.end..buffer.len()) {
prompt.push_str(chunk);
}
prompt.push('\n');
if range.is_empty() {
writeln!(
prompt,
"Assume the cursor is located where the `<|START|>` span is."
)
.unwrap();
writeln!(
prompt,
"{content_type} can't be replaced, so assume your answer will be inserted at the cursor.",
)
.unwrap();
writeln!(
prompt,
"Generate {content_type} based on the users prompt: {user_prompt}",
)
.unwrap();
} else {
writeln!(prompt, "Modify the user's selected {content_type} based upon the users prompt: '{user_prompt}'").unwrap();
writeln!(prompt, "You must reply with only the adjusted {content_type} (within the '<|START|' and '|END|>' spans) not the entire file.").unwrap();
writeln!(
prompt,
"Double check that you only return code and not the '<|START|' and '|END|'> spans"
)
.unwrap();
}
writeln!(prompt, "Never make remarks about the output.").unwrap();
writeln!(
prompt,
"Do not return anything else, except the generated {content_type}."
)
.unwrap();
Ok(prompt)
}

View file

@ -0,0 +1,152 @@
use anyhow::Context;
use collections::HashMap;
use fs::Fs;
use parking_lot::RwLock;
use serde::{Deserialize, Serialize};
use smol::stream::StreamExt;
use std::sync::Arc;
use util::paths::PROMPTS_DIR;
use uuid::Uuid;
use super::prompt::StaticPrompt;
#[derive(Debug, Clone, Copy, Eq, PartialEq, Hash, Serialize, Deserialize)]
pub struct PromptId(pub Uuid);
#[allow(unused)]
impl PromptId {
pub fn new() -> Self {
Self(Uuid::new_v4())
}
}
#[derive(Default, Serialize, Deserialize)]
pub struct PromptLibraryState {
/// A set of prompts that all assistant contexts will start with
default_prompt: Vec<PromptId>,
/// All [Prompt]s loaded into the library
prompts: HashMap<PromptId, StaticPrompt>,
/// Prompts that have been changed but haven't been
/// saved back to the file system
dirty_prompts: Vec<PromptId>,
version: usize,
}
pub struct PromptLibrary {
state: RwLock<PromptLibraryState>,
}
impl Default for PromptLibrary {
fn default() -> Self {
Self::new()
}
}
impl PromptLibrary {
fn new() -> Self {
Self {
state: RwLock::new(PromptLibraryState::default()),
}
}
pub fn prompts(&self) -> Vec<(PromptId, StaticPrompt)> {
let state = self.state.read();
state
.prompts
.iter()
.map(|(id, prompt)| (*id, prompt.clone()))
.collect()
}
pub fn first_prompt_id(&self) -> Option<PromptId> {
let state = self.state.read();
state.prompts.keys().next().cloned()
}
pub fn prompt(&self, id: PromptId) -> Option<StaticPrompt> {
let state = self.state.read();
state.prompts.get(&id).cloned()
}
/// Save the current state of the prompt library to the
/// file system as a JSON file
pub async fn save(&self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
fs.create_dir(&PROMPTS_DIR).await?;
let path = PROMPTS_DIR.join("index.json");
let json = {
let state = self.state.read();
serde_json::to_string(&*state)?
};
fs.atomic_write(path, json).await?;
Ok(())
}
/// Load the state of the prompt library from the file system
/// or create a new one if it doesn't exist
pub async fn load(fs: Arc<dyn Fs>) -> anyhow::Result<Self> {
let path = PROMPTS_DIR.join("index.json");
let state = if fs.is_file(&path).await {
let json = fs.load(&path).await?;
serde_json::from_str(&json)?
} else {
PromptLibraryState::default()
};
let mut prompt_library = Self {
state: RwLock::new(state),
};
prompt_library.load_prompts(fs).await?;
Ok(prompt_library)
}
/// Load all prompts from the file system
/// adding them to the library if they don't already exist
pub async fn load_prompts(&mut self, fs: Arc<dyn Fs>) -> anyhow::Result<()> {
// let current_prompts = self.all_prompt_contents().clone();
// For now, we'll just clear the prompts and reload them all
self.state.get_mut().prompts.clear();
let mut prompt_paths = fs.read_dir(&PROMPTS_DIR).await?;
while let Some(prompt_path) = prompt_paths.next().await {
let prompt_path = prompt_path.with_context(|| "Failed to read prompt path")?;
if !fs.is_file(&prompt_path).await
|| prompt_path.extension().and_then(|ext| ext.to_str()) != Some("md")
{
continue;
}
let json = fs
.load(&prompt_path)
.await
.with_context(|| format!("Failed to load prompt {:?}", prompt_path))?;
let mut static_prompt = StaticPrompt::new(json);
if let Some(file_name) = prompt_path.file_name() {
let file_name = file_name.to_string_lossy().into_owned();
static_prompt.file_name(file_name);
}
let state = self.state.get_mut();
let id = Uuid::new_v4();
state.prompts.insert(PromptId(id), static_prompt);
state.version += 1;
}
// Write any changes back to the file system
self.save(fs.clone()).await?;
Ok(())
}
}

View file

@ -0,0 +1,327 @@
use collections::HashMap;
use editor::Editor;
use fs::Fs;
use gpui::{prelude::FluentBuilder, *};
use language::{language_settings, Buffer, LanguageRegistry};
use picker::{Picker, PickerDelegate};
use std::sync::Arc;
use ui::{prelude::*, IconButtonShape, ListItem, ListItemSpacing};
use util::{ResultExt, TryFutureExt};
use workspace::ModalView;
use super::prompt_library::{PromptId, PromptLibrary};
use crate::prompts::prompt::StaticPrompt;
pub struct PromptManager {
focus_handle: FocusHandle,
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
#[allow(dead_code)]
fs: Arc<dyn Fs>,
picker: View<Picker<PromptManagerDelegate>>,
prompt_editors: HashMap<PromptId, View<Editor>>,
active_prompt_id: Option<PromptId>,
}
impl PromptManager {
pub fn new(
prompt_library: Arc<PromptLibrary>,
language_registry: Arc<LanguageRegistry>,
fs: Arc<dyn Fs>,
cx: &mut ViewContext<Self>,
) -> Self {
let prompt_manager = cx.view().downgrade();
let picker = cx.new_view(|cx| {
Picker::uniform_list(
PromptManagerDelegate {
prompt_manager,
matching_prompts: vec![],
matching_prompt_ids: vec![],
prompt_library: prompt_library.clone(),
selected_index: 0,
},
cx,
)
.max_height(rems(35.75))
.modal(false)
});
let focus_handle = picker.focus_handle(cx);
let mut manager = Self {
focus_handle,
prompt_library,
language_registry,
fs,
picker,
prompt_editors: HashMap::default(),
active_prompt_id: None,
};
manager.active_prompt_id = manager.prompt_library.first_prompt_id();
manager
}
pub fn set_active_prompt(&mut self, prompt_id: Option<PromptId>, cx: &mut ViewContext<Self>) {
self.active_prompt_id = prompt_id;
cx.notify();
}
pub fn focus_active_editor(&self, cx: &mut ViewContext<Self>) {
if let Some(active_prompt_id) = self.active_prompt_id {
if let Some(editor) = self.prompt_editors.get(&active_prompt_id) {
let focus_handle = editor.focus_handle(cx);
cx.focus(&focus_handle)
}
}
}
fn dismiss(&mut self, _: &menu::Cancel, cx: &mut ViewContext<Self>) {
cx.emit(DismissEvent);
}
fn render_prompt_list(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
let picker = self.picker.clone();
v_flex()
.id("prompt-list")
.bg(cx.theme().colors().surface_background)
.h_full()
.w_2_5()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h(rems(1.75))
.w_full()
.flex_none()
.justify_between()
.child(Label::new("Prompt Library").size(LabelSize::Small))
.child(IconButton::new("new-prompt", IconName::Plus).disabled(true)),
)
.child(
v_flex()
.h(rems(38.25))
.flex_grow()
.justify_start()
.child(picker),
)
}
fn set_editor_for_prompt(
&mut self,
prompt_id: PromptId,
cx: &mut ViewContext<Self>,
) -> impl IntoElement {
let prompt_library = self.prompt_library.clone();
let editor_for_prompt = self.prompt_editors.entry(prompt_id).or_insert_with(|| {
cx.new_view(|cx| {
let text = if let Some(prompt) = prompt_library.prompt(prompt_id) {
prompt.content().to_owned()
} else {
"".to_string()
};
let buffer = cx.new_model(|cx| {
let mut buffer = Buffer::local(text, cx);
let markdown = self.language_registry.language_for_name("Markdown");
cx.spawn(|buffer, mut cx| async move {
if let Some(markdown) = markdown.await.log_err() {
_ = buffer.update(&mut cx, |buffer, cx| {
buffer.set_language(Some(markdown), cx);
});
}
})
.detach();
buffer.set_language_registry(self.language_registry.clone());
buffer
});
let mut editor = Editor::for_buffer(buffer, None, cx);
editor.set_soft_wrap_mode(language_settings::SoftWrap::EditorWidth, cx);
editor.set_show_gutter(false, cx);
editor
})
});
editor_for_prompt.clone()
}
}
impl Render for PromptManager {
fn render(&mut self, cx: &mut ViewContext<Self>) -> impl IntoElement {
h_flex()
.key_context("PromptManager")
.track_focus(&self.focus_handle)
.on_action(cx.listener(Self::dismiss))
// .on_action(cx.listener(Self::save_active_prompt))
.elevation_3(cx)
.size_full()
.flex_none()
.w(rems(64.))
.h(rems(40.))
.overflow_hidden()
.child(self.render_prompt_list(cx))
.child(
div().w_3_5().h_full().child(
v_flex()
.id("prompt-editor")
.border_l_1()
.border_color(cx.theme().colors().border)
.bg(cx.theme().colors().editor_background)
.size_full()
.flex_none()
.min_w_64()
.h_full()
.child(
h_flex()
.bg(cx.theme().colors().background)
.p(Spacing::Small.rems(cx))
.border_b_1()
.border_color(cx.theme().colors().border)
.h_7()
.w_full()
.justify_between()
.child(div())
.child(
IconButton::new("dismiss", IconName::Close)
.shape(IconButtonShape::Square)
.on_click(|_, cx| {
cx.dispatch_action(menu::Cancel.boxed_clone());
}),
),
)
.when_some(self.active_prompt_id, |this, active_prompt_id| {
this.child(
h_flex()
.flex_1()
.w_full()
.py(Spacing::Large.rems(cx))
.px(Spacing::XLarge.rems(cx))
.child(self.set_editor_for_prompt(active_prompt_id, cx)),
)
}),
),
)
}
}
impl EventEmitter<DismissEvent> for PromptManager {}
impl ModalView for PromptManager {}
impl FocusableView for PromptManager {
fn focus_handle(&self, _cx: &AppContext) -> gpui::FocusHandle {
self.focus_handle.clone()
}
}
pub struct PromptManagerDelegate {
prompt_manager: WeakView<PromptManager>,
matching_prompts: Vec<Arc<StaticPrompt>>,
matching_prompt_ids: Vec<PromptId>,
prompt_library: Arc<PromptLibrary>,
selected_index: usize,
}
impl PickerDelegate for PromptManagerDelegate {
type ListItem = ListItem;
fn placeholder_text(&self, _cx: &mut WindowContext) -> Arc<str> {
"Find a prompt…".into()
}
fn match_count(&self) -> usize {
self.matching_prompt_ids.len()
}
fn selected_index(&self) -> usize {
self.selected_index
}
fn set_selected_index(&mut self, ix: usize, _: &mut ViewContext<Picker<Self>>) {
self.selected_index = ix;
}
fn selected_index_changed(
&self,
ix: usize,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Box<dyn Fn(&mut WindowContext) + 'static>> {
let prompt_id = self.matching_prompt_ids.get(ix).copied()?;
let prompt_manager = self.prompt_manager.upgrade()?;
Some(Box::new(move |cx| {
prompt_manager.update(cx, |manager, cx| {
manager.set_active_prompt(Some(prompt_id), cx);
})
}))
}
fn update_matches(&mut self, query: String, cx: &mut ViewContext<Picker<Self>>) -> Task<()> {
let prompt_library = self.prompt_library.clone();
cx.spawn(|picker, mut cx| async move {
async {
let prompts = prompt_library.prompts();
let matching_prompts = prompts
.into_iter()
.filter(|(_, prompt)| {
prompt
.content()
.to_lowercase()
.contains(&query.to_lowercase())
})
.collect::<Vec<_>>();
picker.update(&mut cx, |picker, cx| {
picker.delegate.matching_prompt_ids =
matching_prompts.iter().map(|(id, _)| *id).collect();
picker.delegate.matching_prompts = matching_prompts
.into_iter()
.map(|(_, prompt)| Arc::new(prompt))
.collect();
cx.notify();
})?;
anyhow::Ok(())
}
.log_err()
.await;
})
}
fn confirm(&mut self, _: bool, cx: &mut ViewContext<Picker<Self>>) {
let prompt_manager = self.prompt_manager.upgrade().unwrap();
prompt_manager.update(cx, move |manager, cx| manager.focus_active_editor(cx));
}
fn should_dismiss(&self) -> bool {
false
}
fn dismissed(&mut self, cx: &mut ViewContext<Picker<Self>>) {
self.prompt_manager
.update(cx, |_, cx| {
cx.emit(DismissEvent);
})
.ok();
}
fn render_match(
&self,
ix: usize,
selected: bool,
_cx: &mut ViewContext<Picker<Self>>,
) -> Option<Self::ListItem> {
let matching_prompt = self.matching_prompts.get(ix)?;
let prompt = matching_prompt.clone();
Some(
ListItem::new(ix)
.inset(true)
.spacing(ListItemSpacing::Sparse)
.selected(selected)
.child(Label::new(prompt.title().unwrap_or_default().clone())),
)
}
}

View file

@ -1,5 +1,5 @@
use super::{SlashCommand, SlashCommandCleanup, SlashCommandInvocation};
use crate::PromptLibrary;
use crate::prompts::prompt_library::PromptLibrary;
use anyhow::{anyhow, Context, Result};
use futures::channel::oneshot;
use fuzzy::StringMatchCandidate;
@ -42,7 +42,12 @@ impl SlashCommand for PromptSlashCommand {
.prompts()
.into_iter()
.enumerate()
.map(|(ix, prompt)| StringMatchCandidate::new(ix, prompt.title))
.filter_map(|(ix, prompt)| {
prompt
.1
.title()
.map(|title| StringMatchCandidate::new(ix, title.into()))
})
.collect::<Vec<_>>();
let matches = fuzzy::match_strings(
&candidates,
@ -75,9 +80,11 @@ impl SlashCommand for PromptSlashCommand {
let prompt = library
.prompts()
.into_iter()
.find(|prompt| prompt.title == title)
.with_context(|| format!("no prompt found with title {:?}", title))?;
Ok(prompt.prompt)
.filter_map(|prompt| prompt.1.title().map(|title| (title, prompt)))
.find(|(t, _)| t == &title)
.with_context(|| format!("no prompt found with title {:?}", title))?
.1;
Ok(prompt.1.content().to_owned())
});
SlashCommandInvocation {
output,

View file

@ -41,6 +41,25 @@ pub trait FluentBuilder {
})
}
/// Conditionally unwrap and modify self with one closure if the given option is Some, or another if it is None.
fn when_some_else<T>(
self,
option: Option<T>,
then: impl FnOnce(Self, T) -> Self,
otherwise: impl FnOnce(Self) -> Self,
) -> Self
where
Self: Sized,
{
self.map(|this| {
if let Some(value) = option {
then(this, value)
} else {
otherwise(this)
}
})
}
/// Conditionally modify self with one closure or another
fn when_else(
self,

View file

@ -46,6 +46,7 @@ use std::{
};
use util::post_inc;
use util::{measure, ResultExt};
use uuid::Uuid;
mod prompts;
@ -4514,6 +4515,8 @@ pub enum ElementId {
Integer(usize),
/// A string based ID.
Name(SharedString),
/// A UUID.
Uuid(Uuid),
/// An ID that's equated with a focus handle.
FocusHandle(FocusId),
/// A combination of a name and an integer.
@ -4528,6 +4531,7 @@ impl Display for ElementId {
ElementId::Name(name) => write!(f, "{}", name)?,
ElementId::FocusHandle(_) => write!(f, "FocusHandle")?,
ElementId::NamedInteger(s, i) => write!(f, "{}-{}", s, i)?,
ElementId::Uuid(uuid) => write!(f, "{}", uuid)?,
}
Ok(())
@ -4594,6 +4598,12 @@ impl From<(&'static str, u64)> for ElementId {
}
}
impl From<Uuid> for ElementId {
fn from(value: Uuid) -> Self {
Self::Uuid(value)
}
}
impl From<(&'static str, u32)> for ElementId {
fn from((name, id): (&'static str, u32)) -> Self {
ElementId::NamedInteger(name.into(), id as usize)