Improve dev experience for built-in prompts (#16413)
When launching Zed from the CLI via `cargo run`, we'll always prompt load templates from the repo. This restores behavior that I reverted last night in #16403. Also, I've improved the `script/prompts link/unlink` workflow for overriding prompts of your production copy of Zed. Zed now detects when the overrides directory is created or removed, and does the right thing. You can link and unlink repeatedly without restarting Zed. Release Notes: - N/A
This commit is contained in:
parent
7c268d0c6d
commit
c9c5eef8f2
7 changed files with 182 additions and 99 deletions
|
@ -34,6 +34,7 @@ use language_model::{
|
|||
};
|
||||
pub(crate) use model_selector::*;
|
||||
pub use prompts::PromptBuilder;
|
||||
use prompts::PromptLoadingParams;
|
||||
use semantic_index::{CloudEmbeddingProvider, SemanticIndex};
|
||||
use serde::{Deserialize, Serialize};
|
||||
use settings::{update_settings_file, Settings, SettingsStore};
|
||||
|
@ -180,7 +181,12 @@ impl Assistant {
|
|||
}
|
||||
}
|
||||
|
||||
pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<PromptBuilder> {
|
||||
pub fn init(
|
||||
fs: Arc<dyn Fs>,
|
||||
client: Arc<Client>,
|
||||
stdout_is_a_pty: bool,
|
||||
cx: &mut AppContext,
|
||||
) -> Arc<PromptBuilder> {
|
||||
cx.set_global(Assistant::default());
|
||||
AssistantSettings::register(cx);
|
||||
SlashCommandSettings::register(cx);
|
||||
|
@ -217,10 +223,16 @@ pub fn init(fs: Arc<dyn Fs>, client: Arc<Client>, cx: &mut AppContext) -> Arc<Pr
|
|||
assistant_panel::init(cx);
|
||||
context_servers::init(cx);
|
||||
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some((fs.clone(), cx)))
|
||||
.log_err()
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
let prompt_builder = prompts::PromptBuilder::new(Some(PromptLoadingParams {
|
||||
fs: fs.clone(),
|
||||
repo_path: stdout_is_a_pty
|
||||
.then(|| std::env::current_dir().log_err())
|
||||
.flatten(),
|
||||
cx,
|
||||
}))
|
||||
.log_err()
|
||||
.map(Arc::new)
|
||||
.unwrap_or_else(|| Arc::new(prompts::PromptBuilder::new(None).unwrap()));
|
||||
register_slash_commands(Some(prompt_builder.clone()), cx);
|
||||
inline_assistant::init(
|
||||
fs.clone(),
|
||||
|
|
|
@ -473,7 +473,7 @@ async fn test_slash_commands(cx: &mut TestAppContext) {
|
|||
}
|
||||
|
||||
#[gpui::test]
|
||||
async fn test_edit_step_parsing(cx: &mut TestAppContext) {
|
||||
async fn test_workflow_step_parsing(cx: &mut TestAppContext) {
|
||||
cx.update(prompt_library::init);
|
||||
let settings_store = cx.update(SettingsStore::test);
|
||||
cx.set_global(settings_store);
|
||||
|
|
|
@ -1,11 +1,13 @@
|
|||
use anyhow::Result;
|
||||
use assets::Assets;
|
||||
use fs::Fs;
|
||||
use futures::StreamExt;
|
||||
use handlebars::{Handlebars, RenderError, TemplateError};
|
||||
use gpui::AssetSource;
|
||||
use handlebars::{Handlebars, RenderError};
|
||||
use language::BufferSnapshot;
|
||||
use parking_lot::Mutex;
|
||||
use serde::Serialize;
|
||||
use std::{ops::Range, sync::Arc, time::Duration};
|
||||
use std::{ops::Range, path::PathBuf, sync::Arc, time::Duration};
|
||||
use util::ResultExt;
|
||||
|
||||
#[derive(Serialize)]
|
||||
|
@ -38,115 +40,162 @@ pub struct StepResolutionContext {
|
|||
pub step_to_resolve: String,
|
||||
}
|
||||
|
||||
pub struct PromptLoadingParams<'a> {
|
||||
pub fs: Arc<dyn Fs>,
|
||||
pub repo_path: Option<PathBuf>,
|
||||
pub cx: &'a gpui::AppContext,
|
||||
}
|
||||
|
||||
pub struct PromptBuilder {
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
}
|
||||
|
||||
impl PromptBuilder {
|
||||
pub fn new(
|
||||
fs_and_cx: Option<(Arc<dyn Fs>, &gpui::AppContext)>,
|
||||
) -> Result<Self, Box<TemplateError>> {
|
||||
pub fn new(loading_params: Option<PromptLoadingParams>) -> Result<Self> {
|
||||
let mut handlebars = Handlebars::new();
|
||||
Self::register_templates(&mut handlebars)?;
|
||||
Self::register_built_in_templates(&mut handlebars)?;
|
||||
|
||||
let handlebars = Arc::new(Mutex::new(handlebars));
|
||||
|
||||
if let Some((fs, cx)) = fs_and_cx {
|
||||
Self::watch_fs_for_template_overrides(fs, cx, handlebars.clone());
|
||||
if let Some(params) = loading_params {
|
||||
Self::watch_fs_for_template_overrides(params, handlebars.clone());
|
||||
}
|
||||
|
||||
Ok(Self { handlebars })
|
||||
}
|
||||
|
||||
/// Watches the filesystem for changes to prompt template overrides.
|
||||
///
|
||||
/// This function sets up a file watcher on the prompt templates directory. It performs
|
||||
/// an initial scan of the directory and registers any existing template overrides.
|
||||
/// Then it continuously monitors for changes, reloading templates as they are
|
||||
/// modified or added.
|
||||
///
|
||||
/// If the templates directory doesn't exist initially, it waits for it to be created.
|
||||
/// If the directory is removed, it restores the built-in templates and waits for the
|
||||
/// directory to be recreated.
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `params` - A `PromptLoadingParams` struct containing the filesystem, repository path,
|
||||
/// and application context.
|
||||
/// * `handlebars` - An `Arc<Mutex<Handlebars>>` for registering and updating templates.
|
||||
fn watch_fs_for_template_overrides(
|
||||
fs: Arc<dyn Fs>,
|
||||
cx: &gpui::AppContext,
|
||||
mut params: PromptLoadingParams,
|
||||
handlebars: Arc<Mutex<Handlebars<'static>>>,
|
||||
) {
|
||||
let templates_dir = paths::prompt_overrides_dir();
|
||||
|
||||
cx.background_executor()
|
||||
params.repo_path = None;
|
||||
let templates_dir = paths::prompt_overrides_dir(params.repo_path.as_deref());
|
||||
params.cx.background_executor()
|
||||
.spawn(async move {
|
||||
// Create the prompt templates directory if it doesn't exist
|
||||
if !fs.is_dir(templates_dir).await {
|
||||
if let Err(e) = fs.create_dir(templates_dir).await {
|
||||
log::error!("Failed to create prompt templates directory: {}", e);
|
||||
return;
|
||||
let Some(parent_dir) = templates_dir.parent() else {
|
||||
return;
|
||||
};
|
||||
|
||||
let mut found_dir_once = false;
|
||||
loop {
|
||||
// Check if the templates directory exists and handle its status
|
||||
// If it exists, log its presence and check if it's a symlink
|
||||
// If it doesn't exist:
|
||||
// - Log that we're using built-in prompts
|
||||
// - Check if it's a broken symlink and log if so
|
||||
// - Set up a watcher to detect when it's created
|
||||
// After the first check, set the `found_dir_once` flag
|
||||
// This allows us to avoid logging when looping back around after deleting the prompt overrides directory.
|
||||
let dir_status = params.fs.is_dir(&templates_dir).await;
|
||||
let symlink_status = params.fs.read_link(&templates_dir).await.ok();
|
||||
if dir_status {
|
||||
let mut log_message = format!("Prompt template overrides directory found at {}", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
} else {
|
||||
if !found_dir_once {
|
||||
log::info!("No prompt template overrides directory found at {}. Using built-in prompts.", templates_dir.display());
|
||||
if let Some(target) = symlink_status {
|
||||
log::info!("Symlink found pointing to {}, but target is invalid.", target.display());
|
||||
}
|
||||
}
|
||||
|
||||
if params.fs.is_dir(parent_dir).await {
|
||||
let (mut changes, _watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
if changed_paths.iter().any(|p| p == &templates_dir) {
|
||||
let mut log_message = format!("Prompt template overrides directory detected at {}", templates_dir.display());
|
||||
if let Ok(target) = params.fs.read_link(&templates_dir).await {
|
||||
log_message.push_str(" -> ");
|
||||
log_message.push_str(&target.display().to_string());
|
||||
}
|
||||
log::info!("{}.", log_message);
|
||||
break;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Initial scan of the prompts directory
|
||||
if let Ok(mut entries) = fs.read_dir(templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
found_dir_once = true;
|
||||
|
||||
match handlebars.lock().register_template_string(&file_name, content) {
|
||||
Ok(_) => {
|
||||
log::info!(
|
||||
"Successfully registered template override: {} ({})",
|
||||
file_name,
|
||||
file_path.display()
|
||||
);
|
||||
},
|
||||
Err(e) => {
|
||||
log::error!(
|
||||
"Failed to register template during initial scan: {} ({})",
|
||||
e,
|
||||
file_path.display()
|
||||
);
|
||||
},
|
||||
// Initial scan of the prompt overrides directory
|
||||
if let Ok(mut entries) = params.fs.read_dir(&templates_dir).await {
|
||||
while let Some(Ok(file_path)) = entries.next().await {
|
||||
if file_path.to_string_lossy().ends_with(".hbs") {
|
||||
if let Ok(content) = params.fs.load(&file_path).await {
|
||||
let file_name = file_path.file_stem().unwrap().to_string_lossy();
|
||||
log::info!("Registering prompt template override: {}", file_name);
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Watch for changes
|
||||
let (mut changes, watcher) = fs.watch(templates_dir, Duration::from_secs(1)).await;
|
||||
while let Some(changed_paths) = changes.next().await {
|
||||
for changed_path in changed_paths {
|
||||
if changed_path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading template: {}", changed_path.display());
|
||||
if let Some(content) = fs.load(&changed_path).await.log_err() {
|
||||
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
|
||||
let file_path = changed_path.to_string_lossy();
|
||||
match handlebars.lock().register_template_string(&file_name, content) {
|
||||
Ok(_) => log::info!(
|
||||
"Successfully reloaded template: {} ({})",
|
||||
file_name,
|
||||
file_path
|
||||
),
|
||||
Err(e) => log::error!(
|
||||
"Failed to register template: {} ({})",
|
||||
e,
|
||||
file_path
|
||||
),
|
||||
// Watch both the parent directory and the template overrides directory:
|
||||
// - Monitor the parent directory to detect if the template overrides directory is deleted.
|
||||
// - Monitor the template overrides directory to re-register templates when they change.
|
||||
// Combine both watch streams into a single stream.
|
||||
let (parent_changes, parent_watcher) = params.fs.watch(parent_dir, Duration::from_secs(1)).await;
|
||||
let (changes, watcher) = params.fs.watch(&templates_dir, Duration::from_secs(1)).await;
|
||||
let mut combined_changes = futures::stream::select(changes, parent_changes);
|
||||
|
||||
while let Some(changed_paths) = combined_changes.next().await {
|
||||
if changed_paths.iter().any(|p| p == &templates_dir) {
|
||||
if !params.fs.is_dir(&templates_dir).await {
|
||||
log::info!("Prompt template overrides directory removed. Restoring built-in prompt templates.");
|
||||
Self::register_built_in_templates(&mut handlebars.lock()).log_err();
|
||||
break;
|
||||
}
|
||||
}
|
||||
for changed_path in changed_paths {
|
||||
if changed_path.starts_with(&templates_dir) && changed_path.extension().map_or(false, |ext| ext == "hbs") {
|
||||
log::info!("Reloading prompt template override: {}", changed_path.display());
|
||||
if let Some(content) = params.fs.load(&changed_path).await.log_err() {
|
||||
let file_name = changed_path.file_stem().unwrap().to_string_lossy();
|
||||
handlebars.lock().register_template_string(&file_name, content).log_err();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
drop(watcher);
|
||||
drop(parent_watcher);
|
||||
}
|
||||
drop(watcher);
|
||||
})
|
||||
.detach();
|
||||
}
|
||||
|
||||
fn register_templates(handlebars: &mut Handlebars) -> Result<(), Box<TemplateError>> {
|
||||
let mut register_template = |id: &str| {
|
||||
let prompt = Assets::get(&format!("prompts/{}.hbs", id))
|
||||
.unwrap_or_else(|| panic!("{} prompt template not found", id))
|
||||
.data;
|
||||
handlebars
|
||||
.register_template_string(id, String::from_utf8_lossy(&prompt))
|
||||
.map_err(Box::new)
|
||||
};
|
||||
|
||||
register_template("content_prompt")?;
|
||||
register_template("terminal_assistant_prompt")?;
|
||||
register_template("edit_workflow")?;
|
||||
register_template("step_resolution")?;
|
||||
fn register_built_in_templates(handlebars: &mut Handlebars) -> Result<()> {
|
||||
for path in Assets.list("prompts")? {
|
||||
if let Some(id) = path.split('/').last().and_then(|s| s.strip_suffix(".hbs")) {
|
||||
if let Some(prompt) = Assets.load(path.as_ref()).log_err().flatten() {
|
||||
log::info!("Registering built-in prompt template: {}", id);
|
||||
handlebars
|
||||
.register_template_string(id, String::from_utf8_lossy(prompt.as_ref()))?
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
|
|
@ -193,15 +193,28 @@ pub fn prompts_dir() -> &'static PathBuf {
|
|||
/// Returns the path to the prompt templates directory.
|
||||
///
|
||||
/// This is where the prompt templates for core features can be overridden with templates.
|
||||
pub fn prompt_overrides_dir() -> &'static PathBuf {
|
||||
static PROMPT_TEMPLATES_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
PROMPT_TEMPLATES_DIR.get_or_init(|| {
|
||||
if cfg!(target_os = "macos") {
|
||||
config_dir().join("prompt_overrides")
|
||||
} else {
|
||||
support_dir().join("prompt_overrides")
|
||||
///
|
||||
/// # Arguments
|
||||
///
|
||||
/// * `dev_mode` - If true, assumes the current working directory is the Zed repository.
|
||||
pub fn prompt_overrides_dir(repo_path: Option<&Path>) -> PathBuf {
|
||||
if let Some(path) = repo_path {
|
||||
let dev_path = path.join("assets").join("prompts");
|
||||
if dev_path.exists() {
|
||||
return dev_path;
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
static PROMPT_TEMPLATES_DIR: OnceLock<PathBuf> = OnceLock::new();
|
||||
PROMPT_TEMPLATES_DIR
|
||||
.get_or_init(|| {
|
||||
if cfg!(target_os = "macos") {
|
||||
config_dir().join("prompt_overrides")
|
||||
} else {
|
||||
support_dir().join("prompt_overrides")
|
||||
}
|
||||
})
|
||||
.clone()
|
||||
}
|
||||
|
||||
/// Returns the path to the semantic search's embeddings directory.
|
||||
|
|
|
@ -187,7 +187,12 @@ fn init_common(app_state: Arc<AppState>, cx: &mut AppContext) -> Arc<PromptBuild
|
|||
);
|
||||
snippet_provider::init(cx);
|
||||
inline_completion_registry::init(app_state.client.telemetry().clone(), cx);
|
||||
let prompt_builder = assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||
let prompt_builder = assistant::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.clone(),
|
||||
stdout_is_a_pty(),
|
||||
cx,
|
||||
);
|
||||
repl::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.telemetry().clone(),
|
||||
|
|
|
@ -3486,7 +3486,7 @@ mod tests {
|
|||
cx,
|
||||
);
|
||||
let prompt_builder =
|
||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), cx);
|
||||
assistant::init(app_state.fs.clone(), app_state.client.clone(), false, cx);
|
||||
repl::init(
|
||||
app_state.fs.clone(),
|
||||
app_state.client.telemetry().clone(),
|
||||
|
|
|
@ -16,8 +16,8 @@
|
|||
# --worktree option. It also provides informative output and error handling.
|
||||
|
||||
if [ "$1" = "link" ]; then
|
||||
# Remove existing link
|
||||
rm -f ~/.config/zed/prompt_overrides
|
||||
# Remove existing link (or directory)
|
||||
rm -rf ~/.config/zed/prompt_overrides
|
||||
if [ "$2" = "--worktree" ]; then
|
||||
# Check if 'prompts' branch exists, create if not
|
||||
if ! git show-ref --quiet refs/heads/prompts; then
|
||||
|
@ -30,17 +30,21 @@ if [ "$1" = "link" ]; then
|
|||
# Create worktree if it doesn't exist
|
||||
git worktree add ../zed_prompts prompts || git worktree add ../zed_prompts -b prompts
|
||||
fi
|
||||
ln -sf "$(pwd)/../zed_prompts/assets/prompts" ~/.config/zed/prompt_overrides
|
||||
ln -sf "$(realpath "$(pwd)/../zed_prompts/assets/prompts")" ~/.config/zed/prompt_overrides
|
||||
echo "Linked $(realpath "$(pwd)/../zed_prompts/assets/prompts") to ~/.config/zed/prompt_overrides"
|
||||
echo -e "\033[0;31mDon't forget you have it linked, or your prompts will go stale\033[0m"
|
||||
echo -e "\033[0;33mDon't forget you have it linked, or your prompts will go stale\033[0m"
|
||||
else
|
||||
ln -sf "$(pwd)/assets/prompts" ~/.config/zed/prompt_overrides
|
||||
echo "Linked $(pwd)/assets/prompts to ~/.config/zed/prompt_overrides"
|
||||
fi
|
||||
elif [ "$1" = "unlink" ]; then
|
||||
# Remove symbolic link
|
||||
rm ~/.config/zed/prompt_overrides
|
||||
echo "Unlinked ~/.config/zed/prompt_overrides"
|
||||
if [ -e ~/.config/zed/prompt_overrides ]; then
|
||||
# Remove symbolic link
|
||||
rm -rf ~/.config/zed/prompt_overrides
|
||||
echo "Unlinked ~/.config/zed/prompt_overrides"
|
||||
else
|
||||
echo -e "\033[33mWarning: No file exists at ~/.config/zed/prompt_overrides\033[0m"
|
||||
fi
|
||||
else
|
||||
echo "This script helps you manage prompt overrides for Zed."
|
||||
echo "You can link this directory to have Zed use the contents of your current repo templates as your active prompts,"
|
||||
|
|
Loading…
Add table
Add a link
Reference in a new issue