Prompt library updates (#11988)

Nate Butler , Nathan Sobo , and Antonio Scandurra created

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>

Change summary

Cargo.lock                                           | 134 ++-
Cargo.toml                                           |   2 
crates/assistant/Cargo.toml                          |   2 
crates/assistant/src/assistant.rs                    |   3 
crates/assistant/src/assistant_panel.rs              | 113 +-
crates/assistant/src/prompt_library.rs               | 454 --------------
crates/assistant/src/prompts.rs                      |  98 --
crates/assistant/src/prompts/prompt.rs               | 278 ++++++++
crates/assistant/src/prompts/prompt_library.rs       | 152 ++++
crates/assistant/src/prompts/prompt_manager.rs       | 327 ++++++++++
crates/assistant/src/slash_command/prompt_command.rs |  17 
crates/gpui/src/util.rs                              |  19 
crates/gpui/src/window.rs                            |  10 
docs/src/assistant-panel.md                          |  36 
14 files changed, 965 insertions(+), 680 deletions(-)

Detailed changes

Cargo.lock πŸ”—

@@ -350,6 +350,7 @@ dependencies = [
  "futures 0.3.28",
  "fuzzy",
  "gpui",
+ "gray_matter",
  "http 0.1.0",
  "indoc",
  "language",
@@ -359,6 +360,7 @@ dependencies = [
  "open_ai",
  "ordered-float 2.10.0",
  "parking_lot",
+ "picker",
  "project",
  "rand 0.8.5",
  "regex",
@@ -2825,7 +2827,7 @@ dependencies = [
  "cranelift-entity",
  "cranelift-isle",
  "gimli",
- "hashbrown 0.14.0",
+ "hashbrown 0.14.5",
  "log",
  "regalloc2",
  "smallvec",
@@ -3145,7 +3147,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "978747c1d849a7d2ee5e8adc0159961c48fb7e5db2f06af6723b80123bb53856"
 dependencies = [
  "cfg-if",
- "hashbrown 0.14.0",
+ "hashbrown 0.14.5",
  "lock_api",
  "once_cell",
  "parking_lot_core",
@@ -3637,11 +3639,12 @@ checksum = "5443807d6dff69373d433ab9ef5378ad8df50ca6298caf15de6e52e24aaf54d5"
 
 [[package]]
 name = "erased-serde"
-version = "0.3.31"
+version = "0.4.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6c138974f9d5e7fe373eb04df7cae98833802ae4b11c24ac7039a21d5af4b26c"
+checksum = "24e2389d65ab4fab27dc2a5de7b191e1f6617d1f1c8855c0dc569c94a4cbb18d"
 dependencies = [
  "serde",
+ "typeid",
 ]
 
 [[package]]
@@ -4522,7 +4525,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6fb8d784f27acf97159b40fc4db5ecd8aa23b9ad5ef69cdd136d3bc80665f0c0"
 dependencies = [
  "fallible-iterator",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "stable_deref_trait",
 ]
 
@@ -4790,6 +4793,18 @@ dependencies = [
  "syn 1.0.109",
 ]
 
+[[package]]
+name = "gray_matter"
+version = "0.2.7"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "7188a951c53316d94711b3d944c28cf79968685d295cbe782494e8811fc75554"
+dependencies = [
+ "serde",
+ "serde_json",
+ "toml 0.5.11",
+ "yaml-rust",
+]
+
 [[package]]
 name = "grid"
 version = "0.13.0"
@@ -4856,9 +4871,9 @@ dependencies = [
 
 [[package]]
 name = "hashbrown"
-version = "0.14.0"
+version = "0.14.5"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "2c6201b9ff9fd90a5a3bac2e56a830d0caa509576f0e503818ee82c181b3437a"
+checksum = "e5274423e17b7c9fc20b6e7e208532f9b19825d82dfd615708b70edd83df41f1"
 dependencies = [
  "ahash 0.8.8",
  "allocator-api2",
@@ -4870,7 +4885,7 @@ version = "0.8.4"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e8094feaf31ff591f651a2664fb9cfd92bba7a60ce3197265e9482ebe753c8f7"
 dependencies = [
- "hashbrown 0.14.0",
+ "hashbrown 0.14.5",
 ]
 
 [[package]]
@@ -5288,12 +5303,12 @@ dependencies = [
 
 [[package]]
 name = "indexmap"
-version = "2.0.0"
+version = "2.2.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d5477fe2230a79769d8dc68e0eabf5437907c0457a5614a9e8dddb67f65eb65d"
+checksum = "168fb715dda47215e360912c096649d23d58bf392ac62f73919e831745e40f26"
 dependencies = [
  "equivalent",
- "hashbrown 0.14.0",
+ "hashbrown 0.14.5",
  "serde",
 ]
 
@@ -5523,9 +5538,9 @@ dependencies = [
 
 [[package]]
 name = "itoa"
-version = "1.0.9"
+version = "1.0.11"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "af150ab688ff2122fcef229be89cb50dd66af9e01a4ff320cc137eecc9bacc38"
+checksum = "49f1f14873335454500d59611f1cf4a4b0f786f9ac11f4312a78e4cf2566695b"
 
 [[package]]
 name = "jni"
@@ -5927,6 +5942,12 @@ dependencies = [
  "safemem",
 ]
 
+[[package]]
+name = "linked-hash-map"
+version = "0.5.6"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "0717cef1bc8b636c6e1c1bbdefc09e6322da8a9321966e8928ef80d20f7f770f"
+
 [[package]]
 name = "linkify"
 version = "0.10.0"
@@ -6030,9 +6051,9 @@ dependencies = [
 
 [[package]]
 name = "log"
-version = "0.4.20"
+version = "0.4.21"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b5e6163cb8c49088c2c36f57875e58ccd8c87c7427f7fbd50ea6710b2f3f2e8f"
+checksum = "90ed8c1e510134f979dbc4f070f87d4313098b704861a105fe34231c70a3901c"
 dependencies = [
  "serde",
  "value-bag",
@@ -6392,7 +6413,7 @@ dependencies = [
  "bitflags 2.4.2",
  "codespan-reporting",
  "hexf-parse",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "log",
  "num-traits",
  "rustc-hash",
@@ -6822,8 +6843,8 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "9cf5f9dd3933bd50a9e1f149ec995f39ae2c496d31fd772c1fd45ebc27e902b0"
 dependencies = [
  "crc32fast",
- "hashbrown 0.14.0",
- "indexmap 2.0.0",
+ "hashbrown 0.14.5",
+ "indexmap 2.2.6",
  "memchr",
 ]
 
@@ -7255,7 +7276,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "e1d3afd2628e69da2be385eb6f2fd57c8ac7977ceeff6dc166ff1657b0e386a9"
 dependencies = [
  "fixedbitset",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
 ]
 
 [[package]]
@@ -8633,9 +8654,9 @@ dependencies = [
 
 [[package]]
 name = "ryu"
-version = "1.0.15"
+version = "1.0.18"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "1ad4cc8da4ef723ed60bced201181d83791ad433213d8c24efffda1eec85d741"
+checksum = "f3cb5ba0dc43242ce17de99c180e96db90b235b8a9fdc9543c96d2209116bd9f"
 
 [[package]]
 name = "safemem"
@@ -8964,18 +8985,18 @@ checksum = "b0293b4b29daaf487284529cc2f5675b8e57c61f70167ba415a463651fd6a918"
 
 [[package]]
 name = "serde"
-version = "1.0.196"
+version = "1.0.202"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "870026e60fa08c69f064aa766c10f10b1d62db9ccd4d0abb206472bee0ce3b32"
+checksum = "226b61a0d411b2ba5ff6d7f73a476ac4f8bb900373459cd00fab8512828ba395"
 dependencies = [
  "serde_derive",
 ]
 
 [[package]]
 name = "serde_derive"
-version = "1.0.196"
+version = "1.0.202"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "33c85360c95e7d137454dc81d9a4ed2b8efd8fbe19cee57357b32b9771fccb67"
+checksum = "6048858004bcff69094cd972ed40a32500f153bd3be9f716b2eed2e8217c4838"
 dependencies = [
  "proc-macro2",
  "quote",
@@ -9004,11 +9025,11 @@ dependencies = [
 
 [[package]]
 name = "serde_json"
-version = "1.0.107"
+version = "1.0.117"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "6b420ce6e3d8bd882e9b243c6eed35dbc9a6110c9769e74b584e0d68d1f20c65"
+checksum = "455182ea6142b14f93f4bc5320a2b31c1f266b66a4a5c858b013302a5d8cbfc3"
 dependencies = [
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "itoa",
  "ryu",
  "serde",
@@ -9020,7 +9041,7 @@ version = "0.1.7"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "26386958a1344003f2b2bcff51a23fbe70461a478ef29247c6c6ab2c1656f53e"
 dependencies = [
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "itoa",
  "ryu",
  "serde",
@@ -9536,7 +9557,7 @@ dependencies = [
  "futures-util",
  "hashlink",
  "hex",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "log",
  "memchr",
  "once_cell",
@@ -10651,7 +10672,7 @@ version = "0.19.15"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "1b5bb770da30e5cbfde35a2d7b9b8a2c4b8ef89548a7a6aeab5c9a576e3e7421"
 dependencies = [
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -10664,7 +10685,7 @@ version = "0.21.1"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "6a8534fd7f78b5405e860340ad6575217ce99f38d4d5c8f2442cb5ecb50090e1"
 dependencies = [
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "toml_datetime",
  "winnow 0.5.15",
 ]
@@ -10675,7 +10696,7 @@ version = "0.22.6"
 source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "2c1b5fd4128cc8d3e0cb74d4ed9a9cc7c7284becd4df68f5f940e1ad123606f6"
 dependencies = [
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "serde",
  "serde_spanned",
  "toml_datetime",
@@ -11099,6 +11120,12 @@ dependencies = [
  "utf-8",
 ]
 
+[[package]]
+name = "typeid"
+version = "1.0.0"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "059d83cc991e7a42fc37bd50941885db0888e34209f8cfd9aab07ddec03bc9cf"
+
 [[package]]
 name = "typenum"
 version = "1.17.0"
@@ -11335,9 +11362,9 @@ checksum = "830b7e5d4d90034032940e4ace0d9a9a057e7a45cd94e6c007832e39edb82f6d"
 
 [[package]]
 name = "value-bag"
-version = "1.4.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "d92ccd67fb88503048c01b59152a04effd0782d035a83a6d256ce6085f08f4a3"
+checksum = "5a84c137d37ab0142f0f2ddfe332651fdbf252e7b7dbb4e67b6c1f1b2e925101"
 dependencies = [
  "value-bag-serde1",
  "value-bag-sval2",
@@ -11345,9 +11372,9 @@ dependencies = [
 
 [[package]]
 name = "value-bag-serde1"
-version = "1.4.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "b0b9f3feef403a50d4d67e9741a6d8fc688bcbb4e4f31bd4aab72cc690284394"
+checksum = "ccacf50c5cb077a9abb723c5bcb5e0754c1a433f1e1de89edc328e2760b6328b"
 dependencies = [
  "erased-serde",
  "serde",
@@ -11356,9 +11383,9 @@ dependencies = [
 
 [[package]]
 name = "value-bag-sval2"
-version = "1.4.1"
+version = "1.9.0"
 source = "registry+https://github.com/rust-lang/crates.io-index"
-checksum = "30b24f4146b6f3361e91cbf527d1fb35e9376c3c0cef72ca5ec5af6d640fad7d"
+checksum = "1785bae486022dfb9703915d42287dcb284c1ee37bd1080eeba78cc04721285b"
 dependencies = [
  "sval",
  "sval_buffer",
@@ -11609,7 +11636,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "0fd83062c17b9f4985d438603cde0a5e8c5c8198201a6937f778b607924c7da2"
 dependencies = [
  "anyhow",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "serde",
  "serde_derive",
  "serde_json",
@@ -11625,7 +11652,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
 checksum = "84e5df6dba6c0d7fafc63a450f1738451ed7a0b52295d83e868218fa286bf708"
 dependencies = [
  "bitflags 2.4.2",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "semver",
 ]
 
@@ -11652,7 +11679,7 @@ dependencies = [
  "cfg-if",
  "encoding_rs",
  "gimli",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "libc",
  "log",
  "object",
@@ -11783,7 +11810,7 @@ dependencies = [
  "cpp_demangle",
  "cranelift-entity",
  "gimli",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "log",
  "object",
  "rustc-demangle",
@@ -11834,7 +11861,7 @@ dependencies = [
  "cc",
  "cfg-if",
  "encoding_rs",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "libc",
  "log",
  "mach",
@@ -11939,7 +11966,7 @@ checksum = "96326c9800fb6c099f50d1bd2126d636fc2f96950e1675acf358c0f52516cd38"
 dependencies = [
  "anyhow",
  "heck 0.4.1",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "wit-parser",
 ]
 
@@ -12634,7 +12661,7 @@ checksum = "d8a39a15d1ae2077688213611209849cad40e9e5cccf6e61951a425850677ff3"
 dependencies = [
  "anyhow",
  "heck 0.4.1",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "wasm-metadata",
  "wit-bindgen-core",
  "wit-component",
@@ -12662,7 +12689,7 @@ checksum = "421c0c848a0660a8c22e2fd217929a0191f14476b68962afd2af89fd22e39825"
 dependencies = [
  "anyhow",
  "bitflags 2.4.2",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "log",
  "serde",
  "serde_derive",
@@ -12681,7 +12708,7 @@ checksum = "196d3ecfc4b759a8573bf86a9b3f8996b304b3732e4c7de81655f875f6efdca6"
 dependencies = [
  "anyhow",
  "id-arena",
- "indexmap 2.0.0",
+ "indexmap 2.2.6",
  "log",
  "semver",
  "serde",
@@ -12848,7 +12875,7 @@ version = "0.4.0"
 source = "git+https://github.com/npmania/xim-rs?rev=27132caffc5b9bc9c432ca4afad184ab6e7c16af#27132caffc5b9bc9c432ca4afad184ab6e7c16af"
 dependencies = [
  "ahash 0.8.8",
- "hashbrown 0.14.0",
+ "hashbrown 0.14.5",
  "log",
  "x11rb",
  "xim-ctext",
@@ -12911,6 +12938,15 @@ dependencies = [
  "toml 0.8.10",
 ]
 
+[[package]]
+name = "yaml-rust"
+version = "0.4.5"
+source = "registry+https://github.com/rust-lang/crates.io-index"
+checksum = "56c1936c4cc7a1c9ab21a1ebb602eb942ba868cbd44a99cb7cdc5892335e1c85"
+dependencies = [
+ "linked-hash-map",
+]
+
 [[package]]
 name = "yansi"
 version = "0.5.1"

Cargo.toml πŸ”—

@@ -376,7 +376,7 @@ unindent = "0.1.7"
 unicase = "2.6"
 unicode-segmentation = "1.10"
 url = "2.2"
-uuid = { version = "1.1.2", features = ["v4", "v5"] }
+uuid = { version = "1.1.2", features = ["v4", "v5", "serde"] }
 wasmparser = "0.201"
 wasm-encoder = "0.201"
 wasmtime = { version = "19.0.0", default-features = false, features = [

crates/assistant/Cargo.toml πŸ”—

@@ -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

crates/assistant/src/assistant.rs πŸ”—

@@ -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};

crates/assistant/src/assistant_panel.rs πŸ”—

@@ -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;
-        };
-
-        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))
-                    });
-                };
-            });
-        };
-    }
+    // 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 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);

crates/assistant/src/prompt_library.rs πŸ”—

@@ -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()
-    }
-}

crates/assistant/src/prompts.rs πŸ”—

@@ -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;

crates/assistant/src/prompts/prompt.rs πŸ”—

@@ -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)
+}

crates/assistant/src/prompts/prompt_library.rs πŸ”—

@@ -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(())
+    }
+}

crates/assistant/src/prompts/prompt_manager.rs πŸ”—

@@ -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())),
+        )
+    }
+}

crates/assistant/src/slash_command/prompt_command.rs πŸ”—

@@ -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,

crates/gpui/src/util.rs πŸ”—

@@ -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,

crates/gpui/src/window.rs πŸ”—

@@ -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)

docs/src/assistant-panel.md πŸ”—

@@ -134,9 +134,11 @@ You can use Ollama with the Zed assistant by making Ollama appear as an OpenAPI
   ```
 5. Restart Zed
 
-## Prompt Manager
+## Prompt Library
 
-Zed has a prompt manager for enabling and disabling custom prompts.
+**Warning: This feature is experimental and the format of prompts is _highly_ likely to change. Use at your own risk!**
+
+Zed has a prompt library that allows you to manage prompts.
 
 These are useful for:
 
@@ -154,26 +156,16 @@ Checked prompts are included in your "default prompt", which can be inserted int
 
 Prompts have a simple format:
 
-```json
-{
-  // ~/.config/zed/prompts/no-comments.json
-  "title": "No comments in code",
-  "version": "1.0",
-  "author": "Nate Butler <iamnbutler@gmail.com>",
-  "languages": ["*"],
-  "prompt": "Do not add inline or doc comments to any returned code. Avoid removing existing comments unless they are no longer accurate due to changes in the code."
-}
-```
-
-Ensure you properly escape your prompt string when creating a new prompt file.
-
-Example:
+```md
+---
+title: Foo
+version: 1.0
+author: Jane Kim <jane@kim.com
+languages: ["*"]
+dependencies: []
+---
 
-```json
-{
-  // ...
-  "prompt": "This project using the gpui crate as it's UI framework for building UI in Rust. When working in Rust files with gpui components, import it's dependencies using `use gpui::{*, prelude::*}`.\n\nWhen a struct has a `#[derive(IntoElement)]` attribute, it is a UI component that must implement `RenderOnce`. Example:\n\n```rust\n#[derive(IntoElement)]\nstruct MyComponent {\n    id: ElementId,\n}\n\nimpl MyComponent {\n    pub fn new(id: impl Into<ElementId>) -> Self {\n        Self { id.into() }\n    }\n}\n\nimpl RenderOnce for MyComponent {\n    fn render(self, cx: &mut WindowContext) -> impl IntoElement {\n        div().id(self.id.clone()).child(text(\"Hello, world!\"))\n    }\n}\n```"
-}
+Foo and bar are terms used in programming to describe generic concepts.
 ```
 
-In the future we'll allow creating and editing prompts directly in the prompt manager, reducing the need to do this by hand.
+In the future we'll allow creating and editing prompts directly in the prompt manager.