11'use client' ;
22
33import { useState , useRef , useEffect , useCallback } from 'react' ;
4+ import YAML from "yaml" ;
45import {
56 getLibrarySkill ,
67 getSkillReference ,
@@ -131,46 +132,38 @@ function parseFrontmatter(content: string): { frontmatter: Frontmatter; body: st
131132 const yamlStr = content . substring ( 4 , endIndex ) ;
132133 const body = content . substring ( endIndex + 4 ) . trimStart ( ) ;
133134
134- // Simple YAML parsing for key: value pairs
135+ // Real YAML parsing: block scalars (`description: >`), quoted strings,
136+ // and escapes must round-trip. A previous hand-rolled line parser read a
137+ // block-scalar description as the literal string ">" and saving then
138+ // destroyed the real description.
135139 const frontmatter : Frontmatter = { } ;
136- for ( const line of yamlStr . split ( '\n' ) ) {
137- const match = line . match ( / ^ ( \w + ) : \s * ( .* ) $ / ) ;
138- if ( match ) {
139- frontmatter [ match [ 1 ] ] = match [ 2 ] . replace ( / ^ [ " ' ] | [ " ' ] $ / g, '' ) ;
140+ try {
141+ const parsed = YAML . parse ( yamlStr ) ;
142+ if ( parsed && typeof parsed === 'object' && ! Array . isArray ( parsed ) ) {
143+ for ( const [ k , v ] of Object . entries ( parsed ) ) {
144+ if ( v === null || v === undefined ) continue ;
145+ frontmatter [ k ] = typeof v === 'string' ? v : YAML . stringify ( v ) . trimEnd ( ) ;
146+ }
140147 }
148+ } catch {
149+ // Unparseable frontmatter: drop the (rare, machine-written) bad block and
150+ // keep the post-delimiter body. Returning the whole file as body would
151+ // duplicate the --- block once any field is added and re-saved.
152+ return { frontmatter : { } , body } ;
141153 }
142154
143155 return { frontmatter, body } ;
144156}
145157
146- // YAML special characters that require quoting
147- const YAML_SPECIAL_CHARS = [ ':' , '[' , ']' , '{' , '}' , '#' , '&' , '*' , '!' , '|' , '>' , "'" , '"' , '%' , '@' , '`' ] ;
148-
149- /**
150- * Format a YAML value, quoting if it contains special characters.
151- * This prevents YAML parsing errors when descriptions contain colons (e.g., "Triggers: foo, bar").
152- */
153- function formatYamlValue ( value : string ) : string {
154- // Check if value needs quoting
155- const needsQuoting = YAML_SPECIAL_CHARS . some ( char => value . includes ( char ) ) ;
156-
157- if ( needsQuoting ) {
158- // Escape backslashes and double quotes, then wrap in quotes
159- const escaped = value . replace ( / \\ / g, '\\\\' ) . replace ( / " / g, '\\"' ) ;
160- return `"${ escaped } "` ;
161- }
162-
163- return value ;
164- }
165-
166158function buildContent ( frontmatter : Frontmatter , body : string ) : string {
167159 const entries = Object . entries ( frontmatter ) . filter ( ( [ , v ] ) => v !== undefined && v !== '' ) ;
168160 if ( entries . length === 0 ) {
169161 return body ;
170162 }
171163
172- // Quote values that contain YAML special characters
173- const yaml = entries . map ( ( [ k , v ] ) => `${ k } : ${ formatYamlValue ( v ! ) } ` ) . join ( '\n' ) ;
164+ // Serialize through the YAML library so quoting/escaping is always valid
165+ // and round-trips with parseFrontmatter.
166+ const yaml = YAML . stringify ( Object . fromEntries ( entries ) , { lineWidth : 0 } ) . trimEnd ( ) ;
174167 return `---\n${ yaml } \n---\n\n${ body } ` ;
175168}
176169
@@ -285,9 +278,6 @@ function FrontmatterEditor({ frontmatter, onChange, disabled }: FrontmatterEdito
285278 onChange ( { ...frontmatter , [ key ] : value || undefined } ) ;
286279 } ;
287280
288- // Check if description contains special YAML characters
289- const descriptionHasSpecialChars = frontmatter . description &&
290- YAML_SPECIAL_CHARS . some ( char => frontmatter . description ?. includes ( char ) ) ;
291281
292282 return (
293283 < div className = "space-y-3 p-3 rounded-lg bg-white/[0.02] border border-white/[0.06]" >
@@ -296,20 +286,14 @@ function FrontmatterEditor({ frontmatter, onChange, disabled }: FrontmatterEdito
296286 < div className = "space-y-2" >
297287 < div >
298288 < label className = "block text-xs text-white/40 mb-1" > Description *</ label >
299- < input
300- type = "text"
289+ < textarea
290+ rows = { 2 }
301291 value = { frontmatter . description || '' }
302292 onChange = { ( e ) => updateField ( 'description' , e . target . value ) }
303293 placeholder = "Brief description of what this skill does"
304- className = "w-full px-3 py-1.5 text-xs rounded-lg bg-white/[0.04] border border-white/[0.08] text-white placeholder:text-white/30 focus:outline-none focus:border-indigo-500/50"
294+ className = "w-full resize-y px-3 py-1.5 text-xs rounded-lg bg-white/[0.04] border border-white/[0.08] text-white placeholder:text-white/30 focus:outline-none focus:border-indigo-500/50"
305295 disabled = { disabled }
306296 />
307- { descriptionHasSpecialChars && (
308- < p className = "text-xs text-blue-400/80 mt-1 flex items-center gap-1" >
309- < Check className = "h-3 w-3" />
310- Contains special characters. This will be auto-quoted for valid YAML
311- </ p >
312- ) }
313297 </ div >
314298
315299 < div className = "grid grid-cols-2 gap-2" >
0 commit comments