@@ -63,16 +63,88 @@ pub async fn discover_prompts_in_excluding(
6363 Ok ( s) => s,
6464 Err ( _) => continue ,
6565 } ;
66+ let ( description, argument_hint, body) = parse_frontmatter ( & content) ;
6667 out. push ( CustomPrompt {
6768 name,
6869 path,
69- content,
70+ content : body,
71+ description,
72+ argument_hint,
7073 } ) ;
7174 }
7275 out. sort_by ( |a, b| a. name . cmp ( & b. name ) ) ;
7376 out
7477}
7578
79+ /// Parse optional YAML-like frontmatter at the beginning of `content`.
80+ /// Supported keys:
81+ /// - `description`: short description shown in the slash popup
82+ /// - `argument-hint` or `argument_hint`: brief hint string shown after the description
83+ /// Returns (description, argument_hint, body_without_frontmatter).
84+ fn parse_frontmatter ( content : & str ) -> ( Option < String > , Option < String > , String ) {
85+ let mut segments = content. split_inclusive ( '\n' ) ;
86+ let Some ( first_segment) = segments. next ( ) else {
87+ return ( None , None , String :: new ( ) ) ;
88+ } ;
89+ let first_line = first_segment. trim_end_matches ( [ '\r' , '\n' ] ) ;
90+ if first_line. trim ( ) != "---" {
91+ return ( None , None , content. to_string ( ) ) ;
92+ }
93+
94+ let mut desc: Option < String > = None ;
95+ let mut hint: Option < String > = None ;
96+ let mut frontmatter_closed = false ;
97+ let mut consumed = first_segment. len ( ) ;
98+
99+ for segment in segments {
100+ let line = segment. trim_end_matches ( [ '\r' , '\n' ] ) ;
101+ let trimmed = line. trim ( ) ;
102+
103+ if trimmed == "---" {
104+ frontmatter_closed = true ;
105+ consumed += segment. len ( ) ;
106+ break ;
107+ }
108+
109+ if trimmed. is_empty ( ) || trimmed. starts_with ( '#' ) {
110+ consumed += segment. len ( ) ;
111+ continue ;
112+ }
113+
114+ if let Some ( ( k, v) ) = trimmed. split_once ( ':' ) {
115+ let key = k. trim ( ) . to_ascii_lowercase ( ) ;
116+ let mut val = v. trim ( ) . to_string ( ) ;
117+ if val. len ( ) >= 2 {
118+ let bytes = val. as_bytes ( ) ;
119+ let first = bytes[ 0 ] ;
120+ let last = bytes[ bytes. len ( ) - 1 ] ;
121+ if ( first == b'\"' && last == b'\"' ) || ( first == b'\'' && last == b'\'' ) {
122+ val = val[ 1 ..val. len ( ) . saturating_sub ( 1 ) ] . to_string ( ) ;
123+ }
124+ }
125+ match key. as_str ( ) {
126+ "description" => desc = Some ( val) ,
127+ "argument-hint" | "argument_hint" => hint = Some ( val) ,
128+ _ => { }
129+ }
130+ }
131+
132+ consumed += segment. len ( ) ;
133+ }
134+
135+ if !frontmatter_closed {
136+ // Unterminated frontmatter: treat input as-is.
137+ return ( None , None , content. to_string ( ) ) ;
138+ }
139+
140+ let body = if consumed >= content. len ( ) {
141+ String :: new ( )
142+ } else {
143+ content[ consumed..] . to_string ( )
144+ } ;
145+ ( desc, hint, body)
146+ }
147+
76148#[ cfg( test) ]
77149mod tests {
78150 use super :: * ;
@@ -124,4 +196,31 @@ mod tests {
124196 let names: Vec < String > = found. into_iter ( ) . map ( |e| e. name ) . collect ( ) ;
125197 assert_eq ! ( names, vec![ "good" ] ) ;
126198 }
199+
200+ #[ tokio:: test]
201+ async fn parses_frontmatter_and_strips_from_body ( ) {
202+ let tmp = tempdir ( ) . expect ( "create TempDir" ) ;
203+ let dir = tmp. path ( ) ;
204+ let file = dir. join ( "withmeta.md" ) ;
205+ let text = "---\n name: ignored\n description: \" Quick review command\" \n argument-hint: \" [file] [priority]\" \n ---\n Actual body with $1 and $ARGUMENTS" ;
206+ fs:: write ( & file, text) . unwrap ( ) ;
207+
208+ let found = discover_prompts_in ( dir) . await ;
209+ assert_eq ! ( found. len( ) , 1 ) ;
210+ let p = & found[ 0 ] ;
211+ assert_eq ! ( p. name, "withmeta" ) ;
212+ assert_eq ! ( p. description. as_deref( ) , Some ( "Quick review command" ) ) ;
213+ assert_eq ! ( p. argument_hint. as_deref( ) , Some ( "[file] [priority]" ) ) ;
214+ // Body should not include the frontmatter delimiters.
215+ assert_eq ! ( p. content, "Actual body with $1 and $ARGUMENTS" ) ;
216+ }
217+
218+ #[ test]
219+ fn parse_frontmatter_preserves_body_newlines ( ) {
220+ let content = "---\r \n description: \" Line endings\" \r \n argument_hint: \" [arg]\" \r \n ---\r \n First line\r \n Second line\r \n " ;
221+ let ( desc, hint, body) = parse_frontmatter ( content) ;
222+ assert_eq ! ( desc. as_deref( ) , Some ( "Line endings" ) ) ;
223+ assert_eq ! ( hint. as_deref( ) , Some ( "[arg]" ) ) ;
224+ assert_eq ! ( body, "First line\r \n Second line\r \n " ) ;
225+ }
127226}
0 commit comments