+
-
- Short description shown in cards, search, and previews.
-
-
diff --git a/src/lib/skillFrontmatter.test.ts b/src/lib/skillFrontmatter.test.ts
new file mode 100644
index 00000000..0ebd095a
--- /dev/null
+++ b/src/lib/skillFrontmatter.test.ts
@@ -0,0 +1,47 @@
+import { describe, expect, it } from "vitest";
+import {
+ extractSkillFrontmatterDescription,
+ truncateSkillPublishSummary,
+} from "./skillFrontmatter";
+
+describe("extractSkillFrontmatterDescription", () => {
+ it("reads a top-level description field", () => {
+ const content = `---
+name: demo
+description: Automate recurring workflows.
+---
+# Demo`;
+
+ expect(extractSkillFrontmatterDescription(content)).toBe("Automate recurring workflows.");
+ });
+
+ it("prefers metadata.description when present", () => {
+ const content = `---
+name: demo
+description: Legacy description.
+metadata:
+ description: Use this skill when the user needs CSV analysis.
+---
+# Demo`;
+
+ expect(extractSkillFrontmatterDescription(content)).toBe(
+ "Use this skill when the user needs CSV analysis.",
+ );
+ });
+
+ it("returns undefined when no description is present", () => {
+ expect(extractSkillFrontmatterDescription("# Demo")).toBeUndefined();
+ expect(extractSkillFrontmatterDescription("---\nname: demo\n---\n# Demo")).toBeUndefined();
+ });
+});
+
+describe("truncateSkillPublishSummary", () => {
+ it("keeps short values unchanged", () => {
+ expect(truncateSkillPublishSummary("Short summary", 300)).toBe("Short summary");
+ });
+
+ it("truncates long values to the publish summary limit", () => {
+ const longDescription = "a".repeat(350);
+ expect(truncateSkillPublishSummary(longDescription, 300)).toHaveLength(300);
+ });
+});
diff --git a/src/lib/skillFrontmatter.ts b/src/lib/skillFrontmatter.ts
index c725dd08..58afc651 100644
--- a/src/lib/skillFrontmatter.ts
+++ b/src/lib/skillFrontmatter.ts
@@ -15,6 +15,12 @@ function parseMetadata(value: unknown) {
}
}
+export function truncateSkillPublishSummary(value: string, maxLength: number) {
+ const trimmed = value.trim();
+ if (!trimmed || trimmed.length <= maxLength) return trimmed;
+ return trimmed.slice(0, maxLength).trimEnd();
+}
+
export function extractSkillFrontmatterDescription(content: string) {
const normalized = content.replace(/\r\n/g, "\n").replace(/\r/g, "\n");
if (!normalized.startsWith("---")) return undefined;
diff --git a/src/routes/skills/publish.tsx b/src/routes/skills/publish.tsx
index 37a81836..54666bd5 100644
--- a/src/routes/skills/publish.tsx
+++ b/src/routes/skills/publish.tsx
@@ -12,7 +12,6 @@ import {
CircleX,
ExternalLink,
FolderOpen,
- Info,
Lock,
Upload as UploadIcon,
X,
@@ -48,7 +47,10 @@ import { Textarea } from "../../components/ui/textarea";
import { UploadDropzoneDecor } from "../../components/UploadDropzoneDecor";
import { VersionInput } from "../../components/VersionInput";
import { setPostPublishFlash } from "../../lib/postPublishFlash";
-import { extractSkillFrontmatterDescription } from "../../lib/skillFrontmatter";
+import {
+ extractSkillFrontmatterDescription,
+ truncateSkillPublishSummary,
+} from "../../lib/skillFrontmatter";
import { getPublicSlugCollision } from "../../lib/slugCollision";
import { expandDroppedItems, expandFilesWithReport } from "../../lib/uploadFiles";
import { useAuthStatus } from "../../lib/useAuthStatus";
@@ -132,7 +134,6 @@ export function Upload() {
tags: false,
license: false,
});
- const [metadataPrefillNote, setMetadataPrefillNote] = useState
(null);
const [version, setVersion] = useState("1.0.0");
const [tags, setTags] = useState("latest");
const [categories, setCategories] = useState([]);
@@ -144,6 +145,7 @@ export function Upload() {
const categoriesTouchedRef = useRef(false);
const topicsTouchedRef = useRef(false);
const summaryTouchedRef = useRef(false);
+ const [summaryRecommendationDismissed, setSummaryRecommendationDismissed] = useState(false);
const [changelogStatus, setChangelogStatus] = useState<"idle" | "loading" | "ready" | "error">(
"idle",
);
@@ -346,7 +348,12 @@ export function Upload() {
useEffect(() => {
if (summaryTouchedRef.current) return;
if (!uploadedSkillSummary) return;
- setSummary((current) => (current === uploadedSkillSummary ? current : uploadedSkillSummary));
+ const nextSummary = truncateSkillPublishSummary(
+ uploadedSkillSummary,
+ SKILL_PUBLISH_SUMMARY_MAX_LENGTH,
+ );
+ if (!nextSummary) return;
+ setSummary((current) => (current === nextSummary ? current : nextSummary));
}, [uploadedSkillSummary]);
useEffect(() => {
@@ -609,12 +616,17 @@ export function Upload() {
);
}
+ function resetSummaryPrefillState() {
+ summaryTouchedRef.current = false;
+ setSummaryRecommendationDismissed(false);
+ }
+
async function applyExpandedFiles(selected: File[]) {
const report = await expandFilesWithReport(selected);
+ resetSummaryPrefillState();
setFiles(report.files);
setIgnoredLocalMetadataPaths(report.ignoredLocalMetadataPaths);
setPendingFileRemovalIndex(null);
- setMetadataPrefillNote(null);
resetFileInput();
if (updateSlug) return;
@@ -624,17 +636,11 @@ export function Upload() {
const nextSlug = slugFromFolderName(folderName);
const nextDisplayName = displayNameFromFolderName(folderName);
- const prefilled: string[] = [];
if (nextSlug && !dirtyFields.slug && !trimmedSlug) {
setSlug(nextSlug);
- prefilled.push("slug");
}
if (nextDisplayName && !dirtyFields.displayName && !trimmedName) {
setDisplayName(nextDisplayName);
- prefilled.push("display name");
- }
- if (prefilled.length > 0) {
- setMetadataPrefillNote(`Suggested ${prefilled.join(" and ")} from the selected folder.`);
}
}
@@ -651,10 +657,10 @@ export function Upload() {
}
function clearSelectedFiles() {
+ resetSummaryPrefillState();
setFiles([]);
setIgnoredLocalMetadataPaths([]);
setPendingFileRemovalIndex(null);
- setMetadataPrefillNote(null);
resetFileInput();
}
@@ -1045,7 +1051,6 @@ export function Upload() {
}
onChange={(event) => {
markFieldDirty("displayName");
- setMetadataPrefillNote(null);
setDisplayName(event.target.value);
}}
placeholder="My skill"
@@ -1067,7 +1072,6 @@ export function Upload() {
className={showSlugStatusIcon ? "pr-10" : undefined}
onChange={(event) => {
markFieldDirty("slug");
- setMetadataPrefillNote(null);
setSlug(event.target.value);
}}
placeholder="skill-name"
@@ -1103,20 +1107,46 @@ export function Upload() {
id="skillSummary"
value={summary}
disabled={isSubmitting}
+ recommendation={
+ files.length > 0 &&
+ Boolean(uploadedSkillSummary) &&
+ !summaryTouchedRef.current &&
+ !summaryRecommendationDismissed
+ }
+ onDismissRecommendation={() => {
+ setSummaryRecommendationDismissed(true);
+ }}
onChange={(nextSummary) => {
summaryTouchedRef.current = true;
setSummary(nextSummary);
}}
/>
- {metadataPrefillNote ? (
-
@@ -1153,69 +1183,41 @@ export function Upload() {
) : null}
-
-