Skip to repository content

tenant.openagents/omega

No repository description is available.

OpenAgents Git authority 2026-07-28T01:55:26.787Z Public web read
NIP-34 coordinate30617:7649603503856e5148d571eac2766b288a8ff1e9e35d380337a1d2b0015b4f92:omega
MaintainersHidden in public view
References2 branches · 1 tag
Read-only clonegit clone https://openagents.com/git/tenant.openagents/omega.git
Browse files

prompt_store.rs

396 lines · 12.1 KB · rust
1mod prompts;
2pub mod rules_to_skills_migration;
3
4use anyhow::{Result, anyhow};
5use chrono::{DateTime, Utc};
6use collections::HashMap;
7use futures::FutureExt as _;
8use futures::future::Shared;
9
10use gpui::{App, AppContext, Entity, Global, ReadGlobal, SharedString, Task};
11use heed::{
12    Database, RoTxn,
13    types::{SerdeBincode, SerdeJson, Str},
14};
15use parking_lot::RwLock;
16pub use prompts::*;
17
18use serde::{Deserialize, Serialize};
19use std::{future::Future, path::PathBuf, sync::Arc};
20use strum::{EnumIter, IntoEnumIterator as _};
21use text::LineEnding;
22use util::ResultExt;
23use uuid::Uuid;
24
25/// Init starts loading the PromptStore in the background and assigns
26/// a shared future to a global.
27pub fn init(cx: &mut App) {
28    let db_path = paths::prompts_dir().join("prompts-library-db.0.mdb");
29    let prompt_store_task = PromptStore::new(db_path, cx);
30    let prompt_store_entity_task = cx
31        .spawn(async move |cx| {
32            prompt_store_task
33                .await
34                .map(|prompt_store| cx.new(|_cx| prompt_store))
35                .map_err(Arc::new)
36        })
37        .shared();
38    cx.set_global(GlobalPromptStore(prompt_store_entity_task))
39}
40
41#[derive(Clone, Debug, Serialize, Deserialize)]
42pub struct PromptMetadata {
43    pub id: PromptId,
44    pub title: Option<SharedString>,
45    pub default: bool,
46    pub saved_at: DateTime<Utc>,
47}
48
49impl PromptMetadata {
50    fn builtin(builtin: BuiltInPrompt) -> Self {
51        Self {
52            id: PromptId::BuiltIn(builtin),
53            title: Some(builtin.title().into()),
54            default: false,
55            saved_at: DateTime::default(),
56        }
57    }
58}
59
60/// Built-in prompts that have default content and can be customized by users.
61#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize, EnumIter)]
62pub enum BuiltInPrompt {
63    CommitMessage,
64}
65
66impl BuiltInPrompt {
67    pub fn title(&self) -> &'static str {
68        match self {
69            Self::CommitMessage => "Commit message",
70        }
71    }
72
73    /// Returns the default content for this built-in prompt.
74    pub fn default_content(&self) -> &'static str {
75        match self {
76            Self::CommitMessage => include_str!("../../git_ui/src/commit_message_prompt.txt"),
77        }
78    }
79}
80
81impl std::fmt::Display for BuiltInPrompt {
82    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
83        match self {
84            Self::CommitMessage => write!(f, "Commit message"),
85        }
86    }
87}
88
89#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
90#[serde(tag = "kind")]
91pub enum PromptId {
92    User { uuid: UserPromptId },
93    BuiltIn(BuiltInPrompt),
94}
95
96impl PromptId {
97    pub fn new() -> PromptId {
98        UserPromptId::new().into()
99    }
100
101    pub fn as_user(&self) -> Option<UserPromptId> {
102        match self {
103            Self::User { uuid } => Some(*uuid),
104            Self::BuiltIn { .. } => None,
105        }
106    }
107
108    pub fn as_built_in(&self) -> Option<BuiltInPrompt> {
109        match self {
110            Self::User { .. } => None,
111            Self::BuiltIn(builtin) => Some(*builtin),
112        }
113    }
114
115    pub fn is_built_in(&self) -> bool {
116        matches!(self, Self::BuiltIn { .. })
117    }
118}
119
120impl From<BuiltInPrompt> for PromptId {
121    fn from(builtin: BuiltInPrompt) -> Self {
122        PromptId::BuiltIn(builtin)
123    }
124}
125
126impl From<UserPromptId> for PromptId {
127    fn from(uuid: UserPromptId) -> Self {
128        PromptId::User { uuid }
129    }
130}
131
132#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash, Serialize, Deserialize)]
133#[serde(transparent)]
134pub struct UserPromptId(pub Uuid);
135
136impl UserPromptId {
137    pub fn new() -> UserPromptId {
138        UserPromptId(Uuid::new_v4())
139    }
140}
141
142impl From<Uuid> for UserPromptId {
143    fn from(uuid: Uuid) -> Self {
144        UserPromptId(uuid)
145    }
146}
147
148impl std::fmt::Display for PromptId {
149    fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
150        match self {
151            PromptId::User { uuid } => write!(f, "{}", uuid.0),
152            PromptId::BuiltIn(builtin) => write!(f, "{}", builtin),
153        }
154    }
155}
156
157pub struct PromptStore {
158    env: heed::Env,
159    metadata_cache: RwLock<MetadataCache>,
160    bodies: Database<SerdeJson<PromptId>, Str>,
161}
162
163#[derive(Default)]
164struct MetadataCache {
165    metadata: Vec<PromptMetadata>,
166    metadata_by_id: HashMap<PromptId, PromptMetadata>,
167}
168
169impl MetadataCache {
170    fn from_db(
171        db: Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
172        txn: &RoTxn,
173    ) -> Result<Self> {
174        let mut cache = MetadataCache::default();
175        for result in db.iter(txn)? {
176            // Fail-open: skip records that can't be decoded (e.g. from a different branch)
177            // rather than failing the entire prompt store initialization.
178            let Ok((prompt_id, metadata)) = result else {
179                log::warn!(
180                    "Skipping unreadable prompt record in database: {:?}",
181                    result.err()
182                );
183                continue;
184            };
185            cache.metadata.push(metadata.clone());
186            cache.metadata_by_id.insert(prompt_id, metadata);
187        }
188
189        // Insert all the built-in prompts that were not customized by the user
190        for builtin in BuiltInPrompt::iter() {
191            let builtin_id = PromptId::BuiltIn(builtin);
192            if !cache.metadata_by_id.contains_key(&builtin_id) {
193                let metadata = PromptMetadata::builtin(builtin);
194                cache.metadata.push(metadata.clone());
195                cache.metadata_by_id.insert(builtin_id, metadata);
196            }
197        }
198        cache.sort();
199        Ok(cache)
200    }
201
202    fn sort(&mut self) {
203        self.metadata.sort_unstable_by(|a, b| {
204            a.title
205                .cmp(&b.title)
206                .then_with(|| b.saved_at.cmp(&a.saved_at))
207        });
208    }
209}
210
211impl PromptStore {
212    pub fn global(cx: &App) -> impl Future<Output = Result<Entity<Self>>> + use<> {
213        let store = GlobalPromptStore::global(cx).0.clone();
214        async move { store.await.map_err(|err| anyhow!(err)) }
215    }
216
217    pub fn new(db_path: PathBuf, cx: &App) -> Task<Result<Self>> {
218        cx.background_spawn(async move {
219            std::fs::create_dir_all(&db_path)?;
220
221            let db_env = unsafe {
222                heed::EnvOpenOptions::new()
223                    .map_size(1024 * 1024 * 1024) // 1GB
224                    .max_dbs(4) // Metadata and bodies (possibly v1 of both as well)
225                    .open(db_path)?
226            };
227
228            let mut txn = db_env.write_txn()?;
229            let metadata = db_env.create_database(&mut txn, Some("metadata.v2"))?;
230            let bodies = db_env.create_database(&mut txn, Some("bodies.v2"))?;
231            txn.commit()?;
232
233            Self::upgrade_dbs(&db_env, metadata, bodies).log_err();
234
235            let txn = db_env.read_txn()?;
236            let metadata_cache = MetadataCache::from_db(metadata, &txn)?;
237            txn.commit()?;
238
239            Ok(PromptStore {
240                env: db_env,
241                metadata_cache: RwLock::new(metadata_cache),
242                bodies,
243            })
244        })
245    }
246
247    fn upgrade_dbs(
248        env: &heed::Env,
249        metadata_db: heed::Database<SerdeJson<PromptId>, SerdeJson<PromptMetadata>>,
250        bodies_db: heed::Database<SerdeJson<PromptId>, Str>,
251    ) -> Result<()> {
252        let mut txn = env.write_txn()?;
253        let Some(bodies_v1_db) = env
254            .open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<String>>(
255                &txn,
256                Some("bodies"),
257            )?
258        else {
259            return Ok(());
260        };
261        let mut bodies_v1 = bodies_v1_db
262            .iter(&txn)?
263            .collect::<heed::Result<HashMap<_, _>>>()?;
264
265        let Some(metadata_v1_db) = env
266            .open_database::<SerdeBincode<PromptIdV1>, SerdeBincode<PromptMetadataV1>>(
267                &txn,
268                Some("metadata"),
269            )?
270        else {
271            return Ok(());
272        };
273        let metadata_v1 = metadata_v1_db
274            .iter(&txn)?
275            .collect::<heed::Result<HashMap<_, _>>>()?;
276
277        for (prompt_id_v1, metadata_v1) in metadata_v1 {
278            let prompt_id_v2 = UserPromptId(prompt_id_v1.0).into();
279            let Some(body_v1) = bodies_v1.remove(&prompt_id_v1) else {
280                continue;
281            };
282
283            if metadata_db
284                .get(&txn, &prompt_id_v2)?
285                .is_none_or(|metadata_v2| metadata_v1.saved_at > metadata_v2.saved_at)
286            {
287                metadata_db.put(
288                    &mut txn,
289                    &prompt_id_v2,
290                    &PromptMetadata {
291                        id: prompt_id_v2,
292                        title: metadata_v1.title.clone(),
293                        default: metadata_v1.default,
294                        saved_at: metadata_v1.saved_at,
295                    },
296                )?;
297                bodies_db.put(&mut txn, &prompt_id_v2, &body_v1)?;
298            }
299        }
300
301        txn.commit()?;
302
303        Ok(())
304    }
305
306    pub fn load(&self, id: PromptId, cx: &App) -> Task<Result<String>> {
307        let env = self.env.clone();
308        let bodies = self.bodies;
309        cx.background_spawn(async move {
310            let txn = env.read_txn()?;
311            let mut prompt: String = match bodies.get(&txn, &id)? {
312                Some(body) => body.into(),
313                None => {
314                    if let Some(built_in) = id.as_built_in() {
315                        built_in.default_content().into()
316                    } else {
317                        anyhow::bail!("prompt not found")
318                    }
319                }
320            };
321            LineEnding::normalize(&mut prompt);
322            Ok(prompt)
323        })
324    }
325
326    pub fn all_prompt_metadata(&self) -> Vec<PromptMetadata> {
327        self.metadata_cache.read().metadata.clone()
328    }
329}
330
331/// Deprecated: Legacy V1 prompt ID format, used only for migrating data from old databases. Use `PromptId` instead.
332#[derive(Clone, Debug, Eq, PartialEq, Serialize, Deserialize, Hash)]
333struct PromptIdV1(Uuid);
334
335impl From<UserPromptId> for PromptIdV1 {
336    fn from(id: UserPromptId) -> Self {
337        PromptIdV1(id.0)
338    }
339}
340
341/// Deprecated: Legacy V1 prompt metadata format, used only for migrating data from old databases. Use `PromptMetadata` instead.
342#[derive(Clone, Debug, Serialize, Deserialize)]
343struct PromptMetadataV1 {
344    id: PromptIdV1,
345    title: Option<SharedString>,
346    default: bool,
347    saved_at: DateTime<Utc>,
348}
349
350/// Wraps a shared future to a prompt store so it can be assigned as a context global.
351pub struct GlobalPromptStore(Shared<Task<Result<Entity<PromptStore>, Arc<anyhow::Error>>>>);
352
353impl Global for GlobalPromptStore {}
354
355#[cfg(test)]
356mod tests {
357    use super::*;
358    use gpui::TestAppContext;
359
360    #[gpui::test]
361    async fn test_built_in_prompt_load(cx: &mut TestAppContext) {
362        cx.executor().allow_parking();
363
364        let temp_dir = tempfile::tempdir().unwrap();
365        let db_path = temp_dir.path().join("prompts-db");
366
367        let store = cx.update(|cx| PromptStore::new(db_path, cx)).await.unwrap();
368        let store = cx.new(|_cx| store);
369
370        let commit_message_id = PromptId::BuiltIn(BuiltInPrompt::CommitMessage);
371
372        let loaded_content = store
373            .update(cx, |store, cx| store.load(commit_message_id, cx))
374            .await
375            .unwrap();
376
377        let mut expected_content = BuiltInPrompt::CommitMessage.default_content().to_string();
378        LineEnding::normalize(&mut expected_content);
379        assert_eq!(
380            loaded_content.trim(),
381            expected_content.trim(),
382            "Loading a built-in prompt not in DB should return default content"
383        );
384
385        assert!(
386            store.read_with(cx, |store, _| {
387                store
388                    .all_prompt_metadata()
389                    .iter()
390                    .any(|metadata| metadata.id == commit_message_id)
391            }),
392            "Built-in prompt should always be in cache"
393        );
394    }
395}
396
Served at tenant.openagents/omega Member data and write actions are omitted.