Skip to content

Commit 6b6bafa

Browse files
authored
Merge pull request Th0rgal#538 from Th0rgal/fix/skills-yaml-roundtrip
fix(skills-editor): real YAML round-trip for skill frontmatter
2 parents fb4f8e7 + 5733f32 commit 6b6bafa

3 files changed

Lines changed: 30 additions & 42 deletions

File tree

dashboard/bun.lock

Lines changed: 3 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

dashboard/package.json

Lines changed: 4 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -45,24 +45,25 @@
4545
"tailwind-merge": "^3.4.0",
4646
"xterm": "^5.3.0",
4747
"xterm-addon-fit": "^0.8.0",
48+
"yaml": "^2.9.0",
4849
"zod": "^4.2.0"
4950
},
5051
"devDependencies": {
5152
"@playwright/test": "^1.57.0",
53+
"@tailwindcss/postcss": "^4",
5254
"@testing-library/dom": "^10.4.0",
5355
"@testing-library/jest-dom": "^6.6.3",
5456
"@testing-library/react": "^16.3.0",
55-
"@vitejs/plugin-react": "^4.5.2",
56-
"@tailwindcss/postcss": "^4",
5757
"@types/node": "^20",
5858
"@types/react": "19.2.7",
5959
"@types/react-dom": "^19",
60+
"@vitejs/plugin-react": "^4.5.2",
6061
"eslint": "^9",
6162
"eslint-config-next": "16.0.10",
63+
"jsdom": "^26.1.0",
6264
"sharp": "^0.34.5",
6365
"tailwindcss": "^4",
6466
"typescript": "^5",
65-
"jsdom": "^26.1.0",
6667
"vitest": "^3.2.4"
6768
}
6869
}

dashboard/src/app/config/skills/page.tsx

Lines changed: 23 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
'use client';
22

33
import { useState, useRef, useEffect, useCallback } from 'react';
4+
import YAML from "yaml";
45
import {
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-
166158
function 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

Comments
 (0)