From dc1203d34c93ed7a73d6f8e19e0c1f5ab312b22c Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Fri, 9 Oct 2026 16:35:59 +0400 Subject: [PATCH] refac --- .../workspace/Skills/SkillEditor.svelte | 9 ++++++- .../workspace/Skills/SkillFiles.svelte | 7 +++++- src/lib/utils/index.ts | 25 ++++++++----------- 3 files changed, 24 insertions(+), 17 deletions(-) diff --git a/src/lib/components/workspace/Skills/SkillEditor.svelte b/src/lib/components/workspace/Skills/SkillEditor.svelte index 132b242a31..35cdc47f2a 100644 --- a/src/lib/components/workspace/Skills/SkillEditor.svelte +++ b/src/lib/components/workspace/Skills/SkillEditor.svelte @@ -16,7 +16,7 @@ let locale = ''; let meta: { i18n?: Record>; [key: string]: any } = {}; import { user } from '$lib/stores'; - import { slugify } from '$lib/utils'; + import { slugify, parseFrontmatter, formatSkillName } from '$lib/utils'; import Spinner from '$lib/components/common/Spinner.svelte'; import { updateSkillAccessGrants } from '$lib/apis/skills'; import { goto, beforeNavigate } from '$app/navigation'; @@ -221,6 +221,12 @@ let accessGrants: any[] = []; let showAccessControlModal = false; + const handleContentChange = (path: string, content: string) => { + if (edit || path !== 'SKILL.md') return; + const fields = parseFrontmatter(content); + if (!name && typeof fields.name === 'string') name = formatSkillName(fields.name); + if (!description && typeof fields.description === 'string') description = fields.description; + }; $: if (!edit && !clone && name) { id = slugify(name); } @@ -454,6 +460,7 @@ bind:dirty={fileDirty} initialPath={new URLSearchParams(location.search).get('path') || 'SKILL.md'} onSave={submitHandler} + onContentChange={handleContentChange} canExport={edit && ($user?.role === 'admin' || $user?.permissions?.workspace?.skills_export)} > diff --git a/src/lib/components/workspace/Skills/SkillFiles.svelte b/src/lib/components/workspace/Skills/SkillFiles.svelte index 794458cc21..0504ec7f92 100644 --- a/src/lib/components/workspace/Skills/SkillFiles.svelte +++ b/src/lib/components/workspace/Skills/SkillFiles.svelte @@ -43,6 +43,7 @@ export let reloadKey = 0; export let initialPath = 'SKILL.md'; export let onSave: () => Promise = async () => {}; + export let onContentChange: (path: string, content: string) => void = () => {}; const i18n = getContext('i18n'); type Draft = SkillFileSummary & { sourcePath?: string; @@ -139,6 +140,7 @@ if (loadedPath && codeEditor) value = codeEditor.getValue(); const file = files.find((f) => f.path === loadedPath); if (file && !readOnly && value !== loadedValue) { + onContentChange(loadedPath, value); file.content = value; file.changed = true; file.size = new TextEncoder().encode(value).length; @@ -158,7 +160,10 @@ }; }; $: if (!$mobile) showFileDrawer = false; - $: if (loadedPath && value !== loadedValue && !readOnly) dirty = true; + $: if (loadedPath && value !== loadedValue && !readOnly) { + dirty = true; + onContentChange(loadedPath, value); + } const blobFor = async (file: Draft) => { if (file.content !== undefined) return new Blob([ diff --git a/src/lib/utils/index.ts b/src/lib/utils/index.ts index 39bcb1908a..598f62d380 100644 --- a/src/lib/utils/index.ts +++ b/src/lib/utils/index.ts @@ -1,6 +1,7 @@ import type { Writable } from 'svelte/store'; import { v4 as uuidv4 } from 'uuid'; import sha256 from 'js-sha256'; +import { parse as parseYaml } from 'yaml'; import DOMPurify, { type UponSanitizeAttributeHookEvent } from 'dompurify'; import { WEBUI_BASE_URL } from '$lib/constants'; import type { FileNavOpenRequest } from '$lib/stores'; @@ -2465,22 +2466,16 @@ export const getCodeBlockContents = (content: string): object => { })) }; }; -export const parseFrontmatter = (content) => { - const match = content.match(/^---\s*\n([\s\S]*?)\n---/); - if (match) { - const frontmatter = {}; - match[1].split('\n').forEach((line) => { - const [key, ...value] = line.split(':'); - if (key && value) { - frontmatter[key.trim()] = value - .join(':') - .trim() - .replace(/^["']|["']$/g, ''); - } - }); - return frontmatter; +export const parseFrontmatter = (content: string): Record => { + const match = content.match(/^\uFEFF?---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/); + if (!match) return {}; + try { + const fields = parseYaml(match[1]); + return fields && typeof fields === 'object' && !Array.isArray(fields) ? fields : {}; + } catch { + // Incomplete frontmatter is normal while typing. + return {}; } - return {}; }; export const formatSkillName = (name) => {