From eeb0a5788327c8c969aab573e7faba45632efb2e Mon Sep 17 00:00:00 2001 From: Daniel Edrisian Date: Mon, 29 Sep 2025 11:42:01 -0700 Subject: [PATCH 1/2] Parse out frontmatter (cherry picked from https://github.com/openai/codex/pull/3565) --- codex-rs/core/src/custom_prompts.rs | 101 +++++++++++++++++- codex-rs/protocol/src/custom_prompts.rs | 6 ++ codex-rs/tui/src/bottom_pane/chat_composer.rs | 2 + codex-rs/tui/src/bottom_pane/command_popup.rs | 6 ++ 4 files changed, 114 insertions(+), 1 deletion(-) diff --git a/codex-rs/core/src/custom_prompts.rs b/codex-rs/core/src/custom_prompts.rs index 357abef55b..d92c683902 100644 --- a/codex-rs/core/src/custom_prompts.rs +++ b/codex-rs/core/src/custom_prompts.rs @@ -63,16 +63,88 @@ pub async fn discover_prompts_in_excluding( Ok(s) => s, Err(_) => continue, }; + let (description, argument_hint, body) = parse_frontmatter(&content); out.push(CustomPrompt { name, path, - content, + content: body, + description, + argument_hint, }); } out.sort_by(|a, b| a.name.cmp(&b.name)); out } +/// Parse optional YAML-like frontmatter at the beginning of `content`. +/// Supported keys: +/// - `description`: short description shown in the slash popup +/// - `argument-hint` or `argument_hint`: brief hint string shown after the description +/// Returns (description, argument_hint, body_without_frontmatter). +fn parse_frontmatter(content: &str) -> (Option, Option, String) { + let mut segments = content.split_inclusive('\n'); + let Some(first_segment) = segments.next() else { + return (None, None, String::new()); + }; + let first_line = first_segment.trim_end_matches(['\r', '\n']); + if first_line.trim() != "---" { + return (None, None, content.to_string()); + } + + let mut desc: Option = None; + let mut hint: Option = None; + let mut frontmatter_closed = false; + let mut consumed = first_segment.len(); + + for segment in segments { + let line = segment.trim_end_matches(['\r', '\n']); + let trimmed = line.trim(); + + if trimmed == "---" { + frontmatter_closed = true; + consumed += segment.len(); + break; + } + + if trimmed.is_empty() || trimmed.starts_with('#') { + consumed += segment.len(); + continue; + } + + if let Some((k, v)) = trimmed.split_once(':') { + let key = k.trim().to_ascii_lowercase(); + let mut val = v.trim().to_string(); + if val.len() >= 2 { + let bytes = val.as_bytes(); + let first = bytes[0]; + let last = bytes[bytes.len() - 1]; + if (first == b'\"' && last == b'\"') || (first == b'\'' && last == b'\'') { + val = val[1..val.len().saturating_sub(1)].to_string(); + } + } + match key.as_str() { + "description" => desc = Some(val), + "argument-hint" | "argument_hint" => hint = Some(val), + _ => {} + } + } + + consumed += segment.len(); + } + + if !frontmatter_closed { + // Unterminated frontmatter: treat input as-is. + return (None, None, content.to_string()); + } + + let body = if consumed >= content.len() { + String::new() + } else { + content[consumed..].to_string() + }; + (desc, hint, body) +} + #[cfg(test)] mod tests { use super::*; @@ -124,4 +196,31 @@ mod tests { let names: Vec = found.into_iter().map(|e| e.name).collect(); assert_eq!(names, vec!["good"]); } + + #[tokio::test] + async fn parses_frontmatter_and_strips_from_body() { + let tmp = tempdir().expect("create TempDir"); + let dir = tmp.path(); + let file = dir.join("withmeta.md"); + let text = "---\nname: ignored\ndescription: \"Quick review command\"\nargument-hint: \"[file] [priority]\"\n---\nActual body with $1 and $ARGUMENTS"; + fs::write(&file, text).unwrap(); + + let found = discover_prompts_in(dir).await; + assert_eq!(found.len(), 1); + let p = &found[0]; + assert_eq!(p.name, "withmeta"); + assert_eq!(p.description.as_deref(), Some("Quick review command")); + assert_eq!(p.argument_hint.as_deref(), Some("[file] [priority]")); + // Body should not include the frontmatter delimiters. + assert_eq!(p.content, "Actual body with $1 and $ARGUMENTS"); + } + + #[test] + fn parse_frontmatter_preserves_body_newlines() { + let content = "---\r\ndescription: \"Line endings\"\r\nargument_hint: \"[arg]\"\r\n---\r\nFirst line\r\nSecond line\r\n"; + let (desc, hint, body) = parse_frontmatter(content); + assert_eq!(desc.as_deref(), Some("Line endings")); + assert_eq!(hint.as_deref(), Some("[arg]")); + assert_eq!(body, "First line\r\nSecond line\r\n"); + } } diff --git a/codex-rs/protocol/src/custom_prompts.rs b/codex-rs/protocol/src/custom_prompts.rs index be402051b5..f7f7961077 100644 --- a/codex-rs/protocol/src/custom_prompts.rs +++ b/codex-rs/protocol/src/custom_prompts.rs @@ -8,4 +8,10 @@ pub struct CustomPrompt { pub name: String, pub path: PathBuf, pub content: String, + // Optional short description shown in the slash popup, typically provided + // via frontmatter in the prompt file. + pub description: Option, + // Optional argument hint (e.g., "[file] [flags]") shown alongside the + // description in the popup when available. + pub argument_hint: Option, } diff --git a/codex-rs/tui/src/bottom_pane/chat_composer.rs b/codex-rs/tui/src/bottom_pane/chat_composer.rs index b3c1eea6d9..f9e9dd769d 100644 --- a/codex-rs/tui/src/bottom_pane/chat_composer.rs +++ b/codex-rs/tui/src/bottom_pane/chat_composer.rs @@ -2258,6 +2258,8 @@ mod tests { name: "my-prompt".to_string(), path: "/tmp/my-prompt.md".to_string().into(), content: prompt_text.to_string(), + description: None, + argument_hint: None, }]); type_chars_humanlike( diff --git a/codex-rs/tui/src/bottom_pane/command_popup.rs b/codex-rs/tui/src/bottom_pane/command_popup.rs index 32216f0776..f002acfe1e 100644 --- a/codex-rs/tui/src/bottom_pane/command_popup.rs +++ b/codex-rs/tui/src/bottom_pane/command_popup.rs @@ -276,11 +276,15 @@ mod tests { name: "foo".to_string(), path: "/tmp/foo.md".to_string().into(), content: "hello from foo".to_string(), + description: None, + argument_hint: None, }, CustomPrompt { name: "bar".to_string(), path: "/tmp/bar.md".to_string().into(), content: "hello from bar".to_string(), + description: None, + argument_hint: None, }, ]; let popup = CommandPopup::new(prompts); @@ -303,6 +307,8 @@ mod tests { name: "init".to_string(), path: "/tmp/init.md".to_string().into(), content: "should be ignored".to_string(), + description: None, + argument_hint: None, }]); let items = popup.filtered_items(); let has_collision_prompt = items.into_iter().any(|it| match it { From 85054383531c7c032835ecec2401e9fa22a8d423 Mon Sep 17 00:00:00 2001 From: Daniel Edrisian Date: Mon, 29 Sep 2025 12:21:30 -0700 Subject: [PATCH 2/2] rm --- codex-rs/protocol/src/custom_prompts.rs | 4 ---- 1 file changed, 4 deletions(-) diff --git a/codex-rs/protocol/src/custom_prompts.rs b/codex-rs/protocol/src/custom_prompts.rs index f7f7961077..7bceb1390f 100644 --- a/codex-rs/protocol/src/custom_prompts.rs +++ b/codex-rs/protocol/src/custom_prompts.rs @@ -8,10 +8,6 @@ pub struct CustomPrompt { pub name: String, pub path: PathBuf, pub content: String, - // Optional short description shown in the slash popup, typically provided - // via frontmatter in the prompt file. pub description: Option, - // Optional argument hint (e.g., "[file] [flags]") shown alongside the - // description in the popup when available. pub argument_hint: Option, }