From 3b389f9e3dd4d280c256bfa8e40f5bbe728db625 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 21 Jun 2026 17:32:35 +0800 Subject: [PATCH 01/11] style(gui): remove Windows title bar divider --- crates/agent-gui/src/components/WindowsTitleBar.tsx | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/crates/agent-gui/src/components/WindowsTitleBar.tsx b/crates/agent-gui/src/components/WindowsTitleBar.tsx index cf41016e..576ed378 100644 --- a/crates/agent-gui/src/components/WindowsTitleBar.tsx +++ b/crates/agent-gui/src/components/WindowsTitleBar.tsx @@ -176,8 +176,8 @@ export function WindowsTitleBar({ appUpdate }: { appUpdate?: AppUpdateController return (
From d8a534c4aed6ccdce117db9790683ae83cc9f829 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 21 Jun 2026 17:34:37 +0800 Subject: [PATCH 02/11] style(gui): space Windows update button from controls --- crates/agent-gui/src/components/WindowsTitleBar.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/crates/agent-gui/src/components/WindowsTitleBar.tsx b/crates/agent-gui/src/components/WindowsTitleBar.tsx index 576ed378..7f7fe89c 100644 --- a/crates/agent-gui/src/components/WindowsTitleBar.tsx +++ b/crates/agent-gui/src/components/WindowsTitleBar.tsx @@ -202,7 +202,7 @@ export function WindowsTitleBar({ appUpdate }: { appUpdate?: AppUpdateController aria-label={t("window.controls")} > {appUpdate ? ( - + ) : null} diff --git a/crates/agent-gui/src/components/project-tools/SshTunnelPanel.tsx b/crates/agent-gui/src/components/project-tools/SshTunnelPanel.tsx index 28a656e0..fa0993b1 100644 --- a/crates/agent-gui/src/components/project-tools/SshTunnelPanel.tsx +++ b/crates/agent-gui/src/components/project-tools/SshTunnelPanel.tsx @@ -235,18 +235,20 @@ export function SshTunnelPanel(props: SshTunnelPanelProps) { const visibleSessions = scope === "project" ? projectSshSessions : sshSessions; const canCreateInScope = scope === "project"; const createHosts = canCreateInScope ? associatedHosts : []; + const hasCreateHosts = createHosts.length > 0; + const canShowCreateButton = canCreateInScope && hasCreateHosts; const selectedCreateHostId = createHosts.some((host) => host.id === createHostId) ? createHostId : (createHosts[0]?.id ?? ""); const selectedCreateHost = createHosts.find((host) => host.id === selectedCreateHostId) ?? null; const selectedHostMessage = selectedCreateHost ? hostStatusMessage(selectedCreateHost, t) : ""; const canCreate = Boolean( - canCreateInScope && selectedCreateHost && !selectedHostMessage && !creating, + canShowCreateButton && selectedCreateHost && !selectedHostMessage && !creating, ); useEffect(() => { - if (canCreateInScope || view !== "create") return; + if (canShowCreateButton || view !== "create") return; setView("list"); - }, [canCreateInScope, view]); + }, [canShowCreateButton, view]); useEffect(() => { if (!active) return; @@ -521,7 +523,6 @@ export function SshTunnelPanel(props: SshTunnelPanelProps) { scope === "project" ? t("projectTools.sshTunnelProjectEmptyHint") : t("projectTools.sshTunnelAllEmptyHint"); - const hasCreateHosts = createHosts.length > 0; const visibleSessionCount = visibleSessions.length; const connectedSessionCount = visibleSessions.filter(sshSessionConnected).length; const statusText = @@ -815,7 +816,7 @@ export function SshTunnelPanel(props: SshTunnelPanelProps) {
{statusText}
- {canCreateInScope ? ( + {canShowCreateButton ? ( From 6b90fcf0df3db64fc45c3ee384a66a75ec209bba Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 21 Jun 2026 18:17:35 +0800 Subject: [PATCH 05/11] fix(settings): localize sidebar nav and move back button --- crates/agent-gateway/web/src/i18n/config.ts | 8 + crates/agent-gateway/web/src/index.css | 19 ++- .../web/src/pages/SettingsPage.tsx | 154 ++++++++++-------- crates/agent-gateway/web/src/styles.css | 23 +++ crates/agent-gui/src/i18n/config.ts | 10 ++ crates/agent-gui/src/index.css | 19 ++- crates/agent-gui/src/pages/SettingsPage.tsx | 148 ++++++++++------- 7 files changed, 256 insertions(+), 125 deletions(-) diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index 08998d9e..85025d0f 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -642,6 +642,10 @@ export const translations: Record> = { "settings.navRemote": "Remote", "settings.navSkills": "Skills", "settings.navMemory": "记忆", + "settings.groupGeneral": "通用", + "settings.groupIntelligence": "智能", + "settings.groupAutomation": "自动化", + "settings.groupConnectivity": "连接", "settings.backToChat": "返回对话", "settings.title": "设置", @@ -2075,6 +2079,10 @@ export const translations: Record> = { "settings.navRemote": "Remote", "settings.navSkills": "Skills", "settings.navMemory": "Memory", + "settings.groupGeneral": "General", + "settings.groupIntelligence": "Intelligence", + "settings.groupAutomation": "Automation", + "settings.groupConnectivity": "Connectivity", "settings.backToChat": "Back to Chat", "settings.title": "Settings", diff --git a/crates/agent-gateway/web/src/index.css b/crates/agent-gateway/web/src/index.css index a2651891..e772b503 100644 --- a/crates/agent-gateway/web/src/index.css +++ b/crates/agent-gateway/web/src/index.css @@ -618,6 +618,22 @@ } } + /* Settings sidebar nav indicator */ + .settings-nav-indicator { + animation: navIndicatorIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); + } + + @keyframes navIndicatorIn { + from { + opacity: 0; + transform: translateY(-50%) scaleY(0); + } + to { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + } + /* Settings page section transitions */ .settings-section-enter { animation: settingsSectionIn 0.28s cubic-bezier(0.16, 1, 0.3, 1); @@ -698,7 +714,8 @@ @media (prefers-reduced-motion: reduce) { .settings-section-enter, - .settings-section-title-enter { + .settings-section-title-enter, + .settings-nav-indicator { animation: none; } diff --git a/crates/agent-gateway/web/src/pages/SettingsPage.tsx b/crates/agent-gateway/web/src/pages/SettingsPage.tsx index 212849bf..9a5949a6 100644 --- a/crates/agent-gateway/web/src/pages/SettingsPage.tsx +++ b/crates/agent-gateway/web/src/pages/SettingsPage.tsx @@ -62,23 +62,26 @@ function NavItem({ icon, label, active, onClick }: NavItemProps) { + +
+
+ +
+
+
{t("settings.title")}
+
LiveAgent
+
+
+ + +
diff --git a/crates/agent-gateway/web/src/styles.css b/crates/agent-gateway/web/src/styles.css index 8223722d..319e917f 100644 --- a/crates/agent-gateway/web/src/styles.css +++ b/crates/agent-gateway/web/src/styles.css @@ -2225,6 +2225,7 @@ html[data-liveagent-webui="gateway"] .project-terminal-viewport .xterm-viewport: html[data-liveagent-webui="gateway"] .settings-nav { order: 2; display: flex; + flex-wrap: nowrap; flex: 0 0 auto; gap: 6px; overflow-x: auto; @@ -2241,18 +2242,40 @@ html[data-liveagent-webui="gateway"] .project-terminal-viewport .xterm-viewport: margin-top: 0 !important; } + html[data-liveagent-webui="gateway"] .settings-nav-group { + display: contents; + margin-top: 0 !important; + } + + html[data-liveagent-webui="gateway"] .settings-nav-group-label { + display: none; + } + + html[data-liveagent-webui="gateway"] .settings-nav-group > div:last-child { + display: contents; + } + + html[data-liveagent-webui="gateway"] .settings-nav-group > div:last-child > * + * { + margin-top: 0 !important; + } + html[data-liveagent-webui="gateway"] .settings-nav-item { width: auto; flex: 0 0 auto; white-space: nowrap; border: 1px solid hsl(var(--border) / 0.5); padding: 8px 10px; + border-radius: 10px; } html[data-liveagent-webui="gateway"] .settings-nav-item-active { border-color: hsl(var(--primary) / 0.35); } + html[data-liveagent-webui="gateway"] .settings-nav-indicator { + display: none; + } + html[data-liveagent-webui="gateway"] .settings-nav-item > div { gap: 8px; } diff --git a/crates/agent-gui/src/i18n/config.ts b/crates/agent-gui/src/i18n/config.ts index d313ac74..a5b07eae 100644 --- a/crates/agent-gui/src/i18n/config.ts +++ b/crates/agent-gui/src/i18n/config.ts @@ -663,6 +663,11 @@ export const translations: Record> = { "settings.navSkills": "Skills", "settings.navMemory": "记忆", "settings.navAbout": "关于", + "settings.groupGeneral": "通用", + "settings.groupIntelligence": "智能", + "settings.groupAutomation": "自动化", + "settings.groupConnectivity": "连接", + "settings.groupOther": "其他", "settings.backToChat": "返回对话", "settings.title": "设置", @@ -2142,6 +2147,11 @@ export const translations: Record> = { "settings.navSkills": "Skills", "settings.navMemory": "Memory", "settings.navAbout": "About", + "settings.groupGeneral": "General", + "settings.groupIntelligence": "Intelligence", + "settings.groupAutomation": "Automation", + "settings.groupConnectivity": "Connectivity", + "settings.groupOther": "Other", "settings.backToChat": "Back to Chat", "settings.title": "Settings", diff --git a/crates/agent-gui/src/index.css b/crates/agent-gui/src/index.css index 95f9c3e2..5d794ee8 100644 --- a/crates/agent-gui/src/index.css +++ b/crates/agent-gui/src/index.css @@ -744,6 +744,22 @@ } } + /* Settings sidebar nav indicator */ + .settings-nav-indicator { + animation: navIndicatorIn 0.2s cubic-bezier(0.16, 1, 0.3, 1); + } + + @keyframes navIndicatorIn { + from { + opacity: 0; + transform: translateY(-50%) scaleY(0); + } + to { + opacity: 1; + transform: translateY(-50%) scaleY(1); + } + } + /* Settings page section transitions */ .settings-section-enter { animation: settingsSectionIn 0.28s cubic-bezier(0.16, 1, 0.3, 1); @@ -858,7 +874,8 @@ @media (prefers-reduced-motion: reduce) { .settings-section-enter, - .settings-section-title-enter { + .settings-section-title-enter, + .settings-nav-indicator { animation: none; } diff --git a/crates/agent-gui/src/pages/SettingsPage.tsx b/crates/agent-gui/src/pages/SettingsPage.tsx index 75befe0f..43468089 100644 --- a/crates/agent-gui/src/pages/SettingsPage.tsx +++ b/crates/agent-gui/src/pages/SettingsPage.tsx @@ -62,54 +62,68 @@ function NavItem({ icon, label, active, onClick }: NavItemProps) { ); } -const NAV_ITEMS_STATIC: Array<{ id: SectionId; icon: ReactNode }> = [ - { - id: "system", - icon: , - }, - { - id: "providers", - icon: , - }, - { - id: "agents", - icon: , - }, - { - id: "memory", - icon: , - }, +type NavGroup = { + labelKey: string; + items: Array<{ id: SectionId; icon: ReactNode }>; +}; + +const NAV_GROUPS: NavGroup[] = [ { - id: "hooks", - icon: , + labelKey: "settings.groupGeneral", + items: [ + { id: "system", icon: }, + { id: "providers", icon: }, + { id: "agents", icon: }, + ], }, { - id: "cron", - icon: , + labelKey: "settings.groupIntelligence", + items: [ + { id: "memory", icon: }, + ], }, { - id: "ssh", - icon: , + labelKey: "settings.groupAutomation", + items: [ + { id: "hooks", icon: }, + { id: "cron", icon: }, + ], }, { - id: "remote", - icon: , + labelKey: "settings.groupConnectivity", + items: [ + { id: "ssh", icon: }, + { id: "remote", icon: }, + ], }, { - id: "about", - icon: , + labelKey: "settings.groupOther", + items: [ + { id: "about", icon: }, + ], }, ]; @@ -139,13 +153,19 @@ export function SettingsPage(props: SettingsPageProps) { }; const hiddenSectionSet = useMemo(() => new Set(hiddenSections), [hiddenSections]); - const navItems = useMemo( + const navGroups = useMemo( () => - NAV_ITEMS_STATIC.filter((item) => !hiddenSectionSet.has(item.id)).map((item) => ({ - ...item, - label: sectionLabels[item.id], - })), - [hiddenSectionSet, sectionLabels], + NAV_GROUPS.map((group) => ({ + label: t(group.labelKey), + items: group.items + .filter((item) => !hiddenSectionSet.has(item.id)) + .map((item) => ({ ...item, label: sectionLabels[item.id] })), + })).filter((group) => group.items.length > 0), + [hiddenSectionSet, sectionLabels, t], + ); + const allNavItems = useMemo( + () => navGroups.flatMap((g) => g.items), + [navGroups], ); useEffect(() => { @@ -153,11 +173,11 @@ export function SettingsPage(props: SettingsPageProps) { }, [initialSection]); useEffect(() => { - if (navItems.some((item) => item.id === section)) { + if (allNavItems.some((item) => item.id === section)) { return; } - setSection(navItems[0]?.id ?? "system"); - }, [navItems, section]); + setSection(allNavItems[0]?.id ?? "system"); + }, [allNavItems, section]); const saveIndicator = getSaveIndicator(saveState, t); const sectionContent = (() => { @@ -198,34 +218,50 @@ export function SettingsPage(props: SettingsPageProps) { return (
-
)}
From b3005a85bc8970ab52bc474c5473afa957fab812 Mon Sep 17 00:00:00 2001 From: su-fen <715041@qq.com> Date: Sun, 21 Jun 2026 19:44:35 +0800 Subject: [PATCH 08/11] feat(skills): add installed skill preview drawer --- crates/agent-gateway/web/src/i18n/config.ts | 34 ++ .../src/pages/skills-hub/SkillsHubPage.tsx | 529 ++++++++++++++++- crates/agent-gui/src/i18n/config.ts | 34 ++ .../src/pages/skills-hub/SkillsHubPage.tsx | 531 +++++++++++++++++- 4 files changed, 1107 insertions(+), 21 deletions(-) diff --git a/crates/agent-gateway/web/src/i18n/config.ts b/crates/agent-gateway/web/src/i18n/config.ts index 728c90db..eb7db6b0 100644 --- a/crates/agent-gateway/web/src/i18n/config.ts +++ b/crates/agent-gateway/web/src/i18n/config.ts @@ -1285,6 +1285,23 @@ export const translations: Record> = { "settings.skillsHubInstallStatusFailed": "读取技能安装状态失败", "settings.skillsHubDeleteFailed": "删除技能失败", "settings.skillsHubDetailLoadFailed": "加载技能详情失败", + "settings.skillsInstalledPreviewOpen": "查看技能详情", + "settings.skillsInstalledPreviewTitle": "已安装技能", + "settings.skillsInstalledPreviewSelected": "已选中", + "settings.skillsInstalledPreviewUnselected": "未选中", + "settings.skillsInstalledPreviewBuiltIn": "内置", + "settings.skillsInstalledPreviewName": "技能名称", + "settings.skillsInstalledPreviewDescription": "技能描述", + "settings.skillsInstalledPreviewNoDescription": "暂无描述", + "settings.skillsInstalledPreviewDetails": "信息", + "settings.skillsInstalledPreviewBaseDir": "目录", + "settings.skillsInstalledPreviewSkillFile": "技能文件", + "settings.skillsInstalledPreviewSource": "来源", + "settings.skillsInstalledPreviewPublished": "发布", + "settings.skillsInstalledPreviewFilePreview": "文件预览", + "settings.skillsInstalledPreviewUnavailable": "文件预览暂不可用。", + "settings.skillsInstalledPreviewEmpty": "这个技能文件没有可预览内容。", + "settings.skillsInstalledPreviewTruncated": "仅显示前 {count} 行。", "settings.skillsSelected": "已选中", "settings.skillsEnable": "启用技能", "settings.skillsAlwaysOn": "内置", @@ -2745,6 +2762,23 @@ export const translations: Record> = { "settings.skillsHubInstallStatusFailed": "Failed to read Skill install status", "settings.skillsHubDeleteFailed": "Failed to delete Skill", "settings.skillsHubDetailLoadFailed": "Failed to load Skill details", + "settings.skillsInstalledPreviewOpen": "View Skill details", + "settings.skillsInstalledPreviewTitle": "Installed Skill", + "settings.skillsInstalledPreviewSelected": "Selected", + "settings.skillsInstalledPreviewUnselected": "Not selected", + "settings.skillsInstalledPreviewBuiltIn": "Built-in", + "settings.skillsInstalledPreviewName": "Skill name", + "settings.skillsInstalledPreviewDescription": "Description", + "settings.skillsInstalledPreviewNoDescription": "No description", + "settings.skillsInstalledPreviewDetails": "Details", + "settings.skillsInstalledPreviewBaseDir": "Directory", + "settings.skillsInstalledPreviewSkillFile": "Skill file", + "settings.skillsInstalledPreviewSource": "Source", + "settings.skillsInstalledPreviewPublished": "Published", + "settings.skillsInstalledPreviewFilePreview": "File preview", + "settings.skillsInstalledPreviewUnavailable": "File preview is unavailable.", + "settings.skillsInstalledPreviewEmpty": "This Skill file has no previewable content.", + "settings.skillsInstalledPreviewTruncated": "Showing the first {count} lines only.", "settings.skillsSelected": "Selected", "settings.skillsEnable": "Enable Skills", "settings.skillsAlwaysOn": "Built-in", diff --git a/crates/agent-gateway/web/src/pages/skills-hub/SkillsHubPage.tsx b/crates/agent-gateway/web/src/pages/skills-hub/SkillsHubPage.tsx index f2e3be27..99f89de2 100644 --- a/crates/agent-gateway/web/src/pages/skills-hub/SkillsHubPage.tsx +++ b/crates/agent-gateway/web/src/pages/skills-hub/SkillsHubPage.tsx @@ -1,6 +1,7 @@ -import { useCallback, useEffect, useMemo, useRef, useState } from "react"; +import { type KeyboardEvent, useCallback, useEffect, useMemo, useRef, useState } from "react"; import { createPortal } from "react-dom"; +import { Markdown } from "../../components/Markdown"; import { AlertTriangle, BookOpen, @@ -29,6 +30,7 @@ import { manageSkill, mergeAlwaysEnabledSkillNames, notifySkillsDiscoveryUpdated, + readSkillText, startSkillInstallJob, type SkillInstallJobSnapshot, type SkillSummary, @@ -48,6 +50,7 @@ import { cn } from "../../lib/shared/utils"; type SkillsHubView = "installed" | "store"; const STORE_PAGE_LIMIT = 24; +const INSTALLED_SKILL_PREVIEW_LINES = 10_000; const TERMINAL_INSTALL_PHASES = new Set(["done", "error"]); const STORE_SORT_OPTIONS: Array<{ value: ClawHubSort; labelKey: string }> = [ { value: "downloads", labelKey: "settings.skillsStoreSortMostDownloaded" }, @@ -65,6 +68,180 @@ type StoreSkillInstallState = { progress: number | null; }; +type InstalledSkillPreviewState = { + skillFile: string; + content: string; + truncated: boolean; + loading: boolean; + error: string | null; +}; + +function emptyInstalledSkillPreviewState(): InstalledSkillPreviewState { + return { + skillFile: "", + content: "", + truncated: false, + loading: false, + error: null, + }; +} + +function normalizePreviewMetadataText(value: string) { + return value + .trim() + .replace(/^#{1,6}\s+/, "") + .replace(/[`*_]/g, "") + .replace(/\s+/g, " ") + .toLowerCase(); +} + +function stripLeadingBlankLines(lines: string[]) { + let index = 0; + while (index < lines.length && !lines[index].trim()) { + index += 1; + } + return lines.slice(index); +} + +function stripReadmeDuplicateSummary(content: string, skill: SkillSummary) { + const expectedName = normalizePreviewMetadataText(skill.name); + const expectedDescription = normalizePreviewMetadataText(skill.description); + let lines = stripLeadingBlankLines(content.split(/\r?\n/)); + + if (lines.length > 0 && normalizePreviewMetadataText(lines[0]) === expectedName) { + lines = stripLeadingBlankLines(lines.slice(1)); + } + + if (expectedDescription && lines.length > 0) { + const paragraph: string[] = []; + let index = 0; + while (index < lines.length && lines[index].trim()) { + paragraph.push(lines[index]); + index += 1; + } + if (normalizePreviewMetadataText(paragraph.join(" ")) === expectedDescription) { + lines = stripLeadingBlankLines(lines.slice(index)); + } + } + + return lines.join("\n").trimStart(); +} + +const FRONTMATTER_PREVIEW_METADATA_KEYS = new Set(["name", "description"]); + +function hasPreviewMetadataFrontmatterField(frontmatterBody: string) { + return frontmatterBody.split(/\r?\n/).some((line) => { + if (/^[ \t]/.test(line)) return false; + const match = line.match(/^([A-Za-z0-9_-]+)\s*:/); + return match ? FRONTMATTER_PREVIEW_METADATA_KEYS.has(match[1].toLowerCase()) : false; + }); +} + +function hasPreviewMetadataInlineFrontmatterField(frontmatterBody: string) { + return Array.from(frontmatterBody.matchAll(/(?:^|\s)([A-Za-z0-9_-]+)\s*:/g)).some((match) => + FRONTMATTER_PREVIEW_METADATA_KEYS.has(match[1].toLowerCase()), + ); +} + +function hasDisplayableFrontmatterContent(frontmatterBody: string) { + return frontmatterBody.split(/\r?\n/).some((line) => { + const trimmed = line.trim(); + return trimmed !== "" && !trimmed.startsWith("#"); + }); +} + +function stripFrontmatterPreviewMetadataFields(frontmatterBody: string) { + const lines = frontmatterBody.split(/\r?\n/); + const nextLines: string[] = []; + let skippingMetadataField = false; + + for (const line of lines) { + const isIndented = /^[ \t]/.test(line); + const trimmed = line.trim(); + const keyMatch = isIndented ? null : line.match(/^([A-Za-z0-9_-]+)\s*:/); + + if (keyMatch) { + skippingMetadataField = FRONTMATTER_PREVIEW_METADATA_KEYS.has(keyMatch[1].toLowerCase()); + if (skippingMetadataField) continue; + } else if (skippingMetadataField) { + if (trimmed === "" || isIndented) continue; + skippingMetadataField = false; + } + + nextLines.push(line); + } + + return nextLines.join("\n").trim(); +} + +function stripInlineFrontmatterPreviewMetadataFields(frontmatterBody: string) { + const matches = Array.from(frontmatterBody.matchAll(/(?:^|\s)([A-Za-z0-9_-]+)\s*:/g)); + if (matches.length === 0) return frontmatterBody.trim(); + + const fields = matches.map((match, index) => { + const rawIndex = match.index ?? 0; + const startsWithSpace = /^\s/.test(match[0]); + const start = rawIndex + (startsWithSpace ? 1 : 0); + const end = + index + 1 < matches.length + ? (matches[index + 1].index ?? frontmatterBody.length) + : frontmatterBody.length; + return { + key: match[1].toLowerCase(), + text: frontmatterBody.slice(start, end).trim(), + }; + }); + + return fields + .filter((field) => !FRONTMATTER_PREVIEW_METADATA_KEYS.has(field.key)) + .map((field) => field.text) + .join(" ") + .trim(); +} + +function stripMarkdownSkillMetadata(content: string, skill: SkillSummary) { + let next = content.replace(/^\uFEFF/, ""); + const frontmatter = next.match(/^---[ \t]*\r?\n([\s\S]*?)\r?\n---[ \t]*(?:\r?\n|$)/); + if (frontmatter && hasPreviewMetadataFrontmatterField(frontmatter[1])) { + const frontmatterBody = stripFrontmatterPreviewMetadataFields(frontmatter[1]); + const rest = next.slice(frontmatter[0].length); + next = hasDisplayableFrontmatterContent(frontmatterBody) + ? `---\n${frontmatterBody}\n---\n${rest}` + : rest; + } else { + const inlineFrontmatter = next.match(/^---[ \t]+([\s\S]*?)[ \t]+---[ \t]*/); + if (inlineFrontmatter && hasPreviewMetadataInlineFrontmatterField(inlineFrontmatter[1])) { + const frontmatterBody = stripInlineFrontmatterPreviewMetadataFields(inlineFrontmatter[1]); + const rest = next.slice(inlineFrontmatter[0].length); + next = frontmatterBody ? `--- ${frontmatterBody} --- ${rest}` : rest; + } + } + return stripReadmeDuplicateSummary(next, skill); +} + +function stripJsonSkillMetadata(content: string) { + try { + const parsed = JSON.parse(content) as unknown; + if (!parsed || Array.isArray(parsed) || typeof parsed !== "object") return content; + const next = { ...(parsed as Record) }; + delete next.name; + delete next.description; + return Object.keys(next).length > 0 ? JSON.stringify(next, null, 2) : ""; + } catch { + return content; + } +} + +function stripInstalledSkillPreviewMetadata(content: string, skill: SkillSummary) { + if (/\.(md|mdx|markdown)$/i.test(skill.skillFile)) { + return stripMarkdownSkillMetadata(content, skill); + } + if (/\.json$/i.test(skill.skillFile)) { + return stripJsonSkillMetadata(content); + } + return content; +} + function ScanActivityDots() { return (