Bundle editing workflow prompt as a read-only built-in prompt (#15615)

Nathan Sobo and Antonio created

Built-in prompts can still be removed from the default prompt, but they
can't be edited and are automatically updated with new Zed releases.

Release Notes:

- N/A

---------

Co-authored-by: Antonio <antonio@zed.dev>

Change summary

assets/prompts/edit_workflow.md              |  87 ++++++++++++++
assets/prompts/step_resolution.md            |   0 
crates/assistant/src/context.rs              |   2 
crates/assistant/src/prompt_library.rs       | 135 +++++++++++++++++++--
crates/assistant/src/system_prompts/edits.md |  86 --------------
5 files changed, 207 insertions(+), 103 deletions(-)

Detailed changes

assets/prompts/edit_workflow.md 🔗

@@ -0,0 +1,87 @@
+Guide the user through code changes in numbered steps that focus on individual functions, type definitions, etc.
+Surround each distinct step in a <step></step> XML tag.
+
+Here's an example of a user-assistant dialog with step numbers and a follow-up question in Rust:
+
+1. User: I want to create a User struct with name and age fields, and a method to print the user's information. Can you help me with that?
+A: Certainly! Let's create a User struct with the specified fields and a method to print the information:
+<step>Define the User struct
+```rust
+struct User {
+    name: String,
+    age: u32,
+}
+```
+</step>
+<step>Implement methods for the User struct
+```rust
+impl User {
+    fn new(name: String, age: u32) -> Self {
+        User { name, age }
+    }
+    fn print_info(&self) {
+        println!("Name: {}, Age: {}", self.name, self.age);
+    }
+}
+```
+</step>
+
+This code defines a User struct and implements two methods:
+- `new`: a constructor to create a new User
+- `print_info`: a method to print the user's information
+
+2. User: Great! Now, how can I create a User instance and call the print_info method?
+A: You can create a User instance and call the print_info method like this:
+
+<step>Create a User instance and call print_info
+```rust
+fn main() {
+    let user = User::new(String::from("Alice"), 30);
+    user.print_info();
+}
+```
+</step>
+
+This will output: `Name: Alice, Age: 30`
+
+3. User: Can you add an email field to the User struct and update the print_info method to include it?
+A: Certainly! Let's modify the User struct and update the print_info method:
+
+<step>Add email field to User struct
+```rust
+struct User {
+    name: String,
+    age: u32,
+    email: String,
+}
+```
+</step>
+
+<step>Update the new method and print_info method
+```rust
+impl User {
+    fn new(name: String, age: u32, email: String) -> Self {
+        User { name, age, email }
+    }
+    fn print_info(&self) {
+        println!("Name: {}, Age: {}, Email: {}", self.name, self.age, self.email);
+    }
+}
+```
+</step>
+
+<step>Update the main function
+```rust
+fn main() {
+    let user = User::new(String::from("Alice"), 30, String::from("alice@example.com"));
+    user.print_info();
+}
+```
+</step>
+
+This will now output: `Name: Alice, Age: 30, Email: alice@example.com`
+The changes include:
+1. Adding the `email` field to the User struct
+2. Updating the `new` method to accept an email parameter
+3. Modifying the `print_info` method to include the email
+4. Updating the main function to provide an email when creating a User instance

crates/assistant/src/context.rs 🔗

@@ -1386,7 +1386,7 @@ impl Context {
             async move {
                 let prompt_store = cx.update(|cx| PromptStore::global(cx))?.await?;
 
-                let mut prompt = prompt_store.operations_prompt();
+                let mut prompt = prompt_store.step_resolution_prompt();
                 prompt.push_str(&step_text);
 
                 request.messages.push(LanguageModelRequestMessage {

crates/assistant/src/prompt_library.rs 🔗

@@ -66,6 +66,11 @@ pub fn init(cx: &mut AppContext) {
     cx.set_global(GlobalPromptStore(prompt_store_future))
 }
 
+const BUILT_IN_TOOLTIP_TEXT: &'static str = concat!(
+    "This prompt supports special functionality.\n",
+    "It's read-only, but you can remove it from your default prompt."
+);
+
 /// This function opens a new prompt library window if one doesn't exist already.
 /// If one exists, it brings it to the foreground.
 ///
@@ -233,15 +238,29 @@ impl PickerDelegate for PromptPickerDelegate {
             .end_hover_slot(
                 h_flex()
                     .gap_2()
-                    .child(
+                    .child(if prompt_id.is_built_in() {
+                        div()
+                            .id("built-in-prompt")
+                            .child(Icon::new(IconName::FileLock).color(Color::Muted))
+                            .tooltip(move |cx| {
+                                Tooltip::with_meta(
+                                    "Built-in prompt",
+                                    None,
+                                    BUILT_IN_TOOLTIP_TEXT,
+                                    cx,
+                                )
+                            })
+                            .into_any()
+                    } else {
                         IconButton::new("delete-prompt", IconName::Trash)
                             .icon_color(Color::Muted)
                             .shape(IconButtonShape::Square)
                             .tooltip(move |cx| Tooltip::text("Delete Prompt", cx))
                             .on_click(cx.listener(move |_, _, cx| {
                                 cx.emit(PromptPickerEvent::Deleted { prompt_id })
-                            })),
-                    )
+                            }))
+                            .into_any_element()
+                    })
                     .child(
                         IconButton::new("toggle-default-prompt", IconName::Sparkle)
                             .selected(default)
@@ -354,6 +373,10 @@ impl PromptLibrary {
     pub fn save_prompt(&mut self, prompt_id: PromptId, cx: &mut ViewContext<Self>) {
         const SAVE_THROTTLE: Duration = Duration::from_millis(500);
 
+        if prompt_id.is_built_in() {
+            return;
+        }
+
         let prompt_metadata = self.store.metadata(prompt_id).unwrap();
         let prompt_editor = self.prompt_editors.get_mut(&prompt_id).unwrap();
         let title = prompt_editor.title_editor.read(cx).text(cx);
@@ -463,6 +486,7 @@ impl PromptLibrary {
                             let mut editor = Editor::auto_width(cx);
                             editor.set_placeholder_text("Untitled", cx);
                             editor.set_text(prompt_metadata.title.unwrap_or_default(), cx);
+                            editor.set_read_only(true);
                             editor
                         });
                         let body_editor = cx.new_view(|cx| {
@@ -474,6 +498,7 @@ impl PromptLibrary {
                             });
 
                             let mut editor = Editor::for_buffer(buffer, None, cx);
+                            editor.set_read_only(true);
                             editor.set_soft_wrap_mode(SoftWrap::EditorWidth, cx);
                             editor.set_show_gutter(false, cx);
                             editor.set_show_wrap_guides(false, cx);
@@ -943,7 +968,23 @@ impl PromptLibrary {
                                                             )
                                                     },
                                                 ))
-                                                .child(
+                                                .child(if prompt_id.is_built_in() {
+                                                    div()
+                                                        .id("built-in-prompt")
+                                                        .child(
+                                                            Icon::new(IconName::FileLock)
+                                                                .color(Color::Muted),
+                                                        )
+                                                        .tooltip(move |cx| {
+                                                            Tooltip::with_meta(
+                                                                "Built-in prompt",
+                                                                None,
+                                                                BUILT_IN_TOOLTIP_TEXT,
+                                                                cx,
+                                                            )
+                                                        })
+                                                        .into_any()
+                                                } else {
                                                     IconButton::new(
                                                         "delete-prompt",
                                                         IconName::Trash,
@@ -961,8 +1002,9 @@ impl PromptLibrary {
                                                     })
                                                     .on_click(|_, cx| {
                                                         cx.dispatch_action(Box::new(DeletePrompt));
-                                                    }),
-                                                )
+                                                    })
+                                                    .into_any_element()
+                                                })
                                                 .child(
                                                     IconButton::new(
                                                         "duplicate-prompt",
@@ -1062,7 +1104,6 @@ pub struct PromptMetadata {
     pub title: Option<SharedString>,
     pub default: bool,
     pub saved_at: DateTime<Utc>,
-    pub built_in: bool,
 }
 
 #[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
@@ -1078,6 +1119,10 @@ impl PromptId {
             uuid: Uuid::new_v4(),
         }
     }
+
+    pub fn is_built_in(&self) -> bool {
+        !matches!(self, PromptId::User { .. })
+    }
 }
 
 pub struct PromptStore {
@@ -1163,13 +1208,17 @@ impl PromptStore {
                 let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
                 txn.commit()?;
 
-                Ok(PromptStore {
+                let store = PromptStore {
                     executor,
                     env: db_env,
                     metadata_cache: RwLock::new(metadata_cache),
                     metadata,
                     bodies,
-                })
+                };
+
+                store.save_built_in_prompts().log_err();
+
+                Ok(store)
             }
         })
     }
@@ -1237,7 +1286,6 @@ impl PromptStore {
                         title: metadata_v1.title.clone(),
                         default: metadata_v1.default,
                         saved_at: metadata_v1.saved_at,
-                        built_in: false,
                     },
                 )?;
                 bodies_db.put(&mut txn, &prompt_id_v2, &body_v1)?;
@@ -1346,12 +1394,15 @@ impl PromptStore {
         default: bool,
         body: Rope,
     ) -> Task<Result<()>> {
+        if id.is_built_in() {
+            return Task::ready(Err(anyhow!("built-in prompts cannot be saved")));
+        }
+
         let prompt_metadata = PromptMetadata {
             id,
             title,
             default,
             saved_at: Utc::now(),
-            built_in: false,
         };
         self.metadata_cache.write().insert(prompt_metadata.clone());
 
@@ -1371,20 +1422,72 @@ impl PromptStore {
         })
     }
 
+    fn save_built_in_prompts(&self) -> Result<()> {
+        self.save_built_in_prompt(
+            PromptId::EditWorkflow,
+            "Built-in: Editing Workflow",
+            "prompts/edit_workflow.md",
+        )?;
+        Ok(())
+    }
+
+    /// Write a built-in prompt to the database, preserving the value of the default field
+    /// if a prompt with this id already exists. This method blocks.
+    fn save_built_in_prompt(
+        &self,
+        id: PromptId,
+        title: impl Into<SharedString>,
+        body_path: &str,
+    ) -> Result<()> {
+        let mut metadata_cache = self.metadata_cache.write();
+        let existing_metadata = metadata_cache.metadata_by_id.get(&id).cloned();
+
+        let prompt_metadata = PromptMetadata {
+            id,
+            title: Some(title.into()),
+            default: existing_metadata.map_or(true, |m| m.default),
+            saved_at: Utc::now(),
+        };
+
+        metadata_cache.insert(prompt_metadata.clone());
+
+        let db_connection = self.env.clone();
+        let bodies = self.bodies;
+        let metadata_db = self.metadata;
+
+        let mut txn = db_connection.write_txn()?;
+        metadata_db.put(&mut txn, &id, &prompt_metadata)?;
+
+        let body = String::from_utf8(Assets.load(body_path)?.unwrap().to_vec())?;
+        bodies.put(&mut txn, &id, &body)?;
+
+        txn.commit()?;
+        Ok(())
+    }
+
     fn save_metadata(
         &self,
         id: PromptId,
-        title: Option<SharedString>,
+        mut title: Option<SharedString>,
         default: bool,
     ) -> Task<Result<()>> {
+        let mut cache = self.metadata_cache.write();
+
+        if id.is_built_in() {
+            title = cache
+                .metadata_by_id
+                .get(&id)
+                .and_then(|metadata| metadata.title.clone());
+        }
+
         let prompt_metadata = PromptMetadata {
             id,
             title,
             default,
             saved_at: Utc::now(),
-            built_in: false,
         };
-        self.metadata_cache.write().insert(prompt_metadata.clone());
+
+        cache.insert(prompt_metadata.clone());
 
         let db_connection = self.env.clone();
         let metadata = self.metadata;
@@ -1402,10 +1505,10 @@ impl PromptStore {
         self.metadata_cache.read().metadata.first().cloned()
     }
 
-    pub fn operations_prompt(&self) -> String {
+    pub fn step_resolution_prompt(&self) -> String {
         String::from_utf8(
             Assets
-                .load("prompts/operations.md")
+                .load("prompts/step_resolution.md")
                 .unwrap()
                 .unwrap()
                 .to_vec(),

crates/assistant/src/system_prompts/edits.md 🔗

@@ -1,86 +0,0 @@
-When the user asks you to suggest edits for a buffer, use a strict template consisting of:
-
-* A markdown code block with the file path as the language identifier.
-* The original code that should be replaced
-* A separator line (`---`)
-* The new text that should replace the original lines
-
-Each code block may only contain an edit for one single contiguous range of text. Use multiple code blocks for multiple edits.
-
-## Example
-
-If you have a buffer with the following lines:
-
-```path/to/file.rs
-fn quicksort(arr: &mut [i32]) {
-    if arr.len() <= 1 {
-        return;
-    }
-    let pivot_index = partition(arr);
-    let (left, right) = arr.split_at_mut(pivot_index);
-    quicksort(left);
-    quicksort(&mut right[1..]);
-}
-
-fn partition(arr: &mut [i32]) -> usize {
-    let last_index = arr.len() - 1;
-    let pivot = arr[last_index];
-    let mut i = 0;
-    for j in 0..last_index {
-        if arr[j] <= pivot {
-            arr.swap(i, j);
-            i += 1;
-        }
-    }
-    arr.swap(i, last_index);
-    i
-}
-```
-
-And you want to replace the for loop inside `partition`, output the following.
-
-```edit path/to/file.rs
-for j in 0..last_index {
-    if arr[j] <= pivot {
-        arr.swap(i, j);
-        i += 1;
-    }
-}
----
-let mut j = 0;
-while j < last_index {
-    if arr[j] <= pivot {
-        arr.swap(i, j);
-        i += 1;
-    }
-    j += 1;
-}
-```
-
-If you wanted to insert comments above the partition function, output the following:
-
-```edit path/to/file.rs
-fn partition(arr: &mut [i32]) -> usize {
----
-// A helper function used for quicksort.
-fn partition(arr: &mut [i32]) -> usize {
-```
-
-If you wanted to delete the partition function, output the following:
-
-```edit path/to/file.rs
-fn partition(arr: &mut [i32]) -> usize {
-    let last_index = arr.len() - 1;
-    let pivot = arr[last_index];
-    let mut i = 0;
-    for j in 0..last_index {
-        if arr[j] <= pivot {
-            arr.swap(i, j);
-            i += 1;
-        }
-    }
-    arr.swap(i, last_index);
-    i
-}
----
-```