From 0dc9eb53669f3006098e590f4a35af0dafb20013 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 00:14:29 -0700 Subject: [PATCH 1/3] Skills directory display names and person-facing seed copy Closes CL-6747 Closes CL-6753 --- apps/web/src/pages/skill-detail-page.tsx | 3 +- apps/web/src/pages/skills-page.tsx | 8 ++- apps/web/src/skill-display-name.test.ts | 27 ++++++++++ apps/web/src/skill-display-name.ts | 13 +++++ apps/web/test/skill-detail-page.test.tsx | 28 ++++++++-- apps/web/test/skills-page.test.tsx | 51 +++++++++++++++++-- .../hub-client/src/default-skills.test.ts | 19 +++++++ packages/hub-client/src/default-skills.ts | 9 ++-- 8 files changed, 143 insertions(+), 15 deletions(-) create mode 100644 apps/web/src/skill-display-name.test.ts create mode 100644 apps/web/src/skill-display-name.ts create mode 100644 packages/hub-client/src/default-skills.test.ts diff --git a/apps/web/src/pages/skill-detail-page.tsx b/apps/web/src/pages/skill-detail-page.tsx index e909c1f38..7d203ec31 100644 --- a/apps/web/src/pages/skill-detail-page.tsx +++ b/apps/web/src/pages/skill-detail-page.tsx @@ -57,6 +57,7 @@ import { useCallback, useEffect, useState, type ReactNode } from "react"; import { useBench } from "../bench-context"; import { SKILLS_PATH_PREFIX, skillIdFromPath } from "../path-ids"; +import { skillDisplayName } from "../skill-display-name"; import { skillVersionSavedBy } from "../skill-version-author"; import { StageTopBar } from "../shell/stage-top-bar"; import { @@ -324,7 +325,7 @@ export function SkillDetailPage({

- {skill.name} + {skillDisplayName(skill)}

Updated {formatRelativeTime(skill.updatedAtIso, now)} diff --git a/apps/web/src/pages/skills-page.tsx b/apps/web/src/pages/skills-page.tsx index 666bb59e7..1992a247e 100644 --- a/apps/web/src/pages/skills-page.tsx +++ b/apps/web/src/pages/skills-page.tsx @@ -46,6 +46,7 @@ import { } from "./create-skill-dialog"; import { useBench } from "../bench-context"; import { SKILLS_PATH_PREFIX } from "../path-ids"; +import { skillDisplayName } from "../skill-display-name"; import { StageTopBar } from "../shell/stage-top-bar"; type RegistryState = @@ -244,13 +245,16 @@ export function SkillsPage({ {...rowActivationProps(() => open(skill.name))} > - {skill.name} + {skillDisplayName(skill)} {skill.description} - + {skill.scope === "tenant" ? "Everyone" : "Only me"} diff --git a/apps/web/src/skill-display-name.test.ts b/apps/web/src/skill-display-name.test.ts new file mode 100644 index 000000000..a0ae65698 --- /dev/null +++ b/apps/web/src/skill-display-name.test.ts @@ -0,0 +1,27 @@ +import { describe, expect, test } from "bun:test"; + +import { skillDisplayName } from "./skill-display-name"; + +describe("skillDisplayName", () => { + test("title-cases a kebab slug when no display title is set", () => { + expect(skillDisplayName({ name: "writing-system-prompts" })).toBe( + "Writing System Prompts", + ); + expect(skillDisplayName({ name: "triage" })).toBe("Triage"); + }); + + test("prefers an explicit displayTitle over the slug", () => { + expect( + skillDisplayName({ + name: "writing-system-prompts", + displayTitle: "Prompt writing", + }), + ).toBe("Prompt writing"); + }); + + test("treats a whitespace-only displayTitle as absent", () => { + expect( + skillDisplayName({ name: "triage", displayTitle: " " }), + ).toBe("Triage"); + }); +}); diff --git a/apps/web/src/skill-display-name.ts b/apps/web/src/skill-display-name.ts new file mode 100644 index 000000000..e4f7f85b9 --- /dev/null +++ b/apps/web/src/skill-display-name.ts @@ -0,0 +1,13 @@ +// Person-facing title for a skill roster/detail surface. The registry id +// stays the kebab slug; what a person reads is either an explicit +// `displayTitle` or a Title Case reading of that slug (CL-6747). +import { humanizeSlug } from "@corbits/chat/display-name"; + +export function skillDisplayName(skill: { + readonly name: string; + readonly displayTitle?: string | null; +}): string { + const titled = skill.displayTitle?.trim(); + if (titled !== undefined && titled !== "") return titled; + return humanizeSlug(skill.name); +} diff --git a/apps/web/test/skill-detail-page.test.tsx b/apps/web/test/skill-detail-page.test.tsx index c0be8f645..570405873 100644 --- a/apps/web/test/skill-detail-page.test.tsx +++ b/apps/web/test/skill-detail-page.test.tsx @@ -129,7 +129,9 @@ async function settle() { }); } -async function mount(): Promise { +async function mount( + props: { readonly name?: string } = {}, +): Promise { container = document.createElement("div"); document.body.appendChild(container); root = createRoot(container); @@ -138,14 +140,14 @@ async function mount(): Promise { , ); }); await settle(); - if (container === null) throw new Error("container went away"); + if (container === null) throw new Error("mount left no container"); return container; } @@ -210,6 +212,26 @@ describe("SkillDetailPage", () => { const el = await mount(); expect(field("skill-body").value).toBe(HEAD_BODY); expect(el.textContent).toContain("Research Buddy"); + // Title is the display name, not the raw kebab slug (CL-6747). + expect(el.querySelector("h1")?.textContent?.trim()).toBe("Triage"); + }); + + test("a kebab skill title is shown title-cased on the detail page", async () => { + const kebab = "writing-system-prompts"; + stubs = { + ...defaultStubs(), + [`GET /api/tenants/${TENANT}/skills/${kebab}`]: ok({ + skill: { ...SKILL, name: kebab }, + pinnedBy: [], + }), + [`GET /api/tenants/${TENANT}/skills/${kebab}/versions`]: ok({ + versions: VERSIONS, + }), + }; + const el = await mount({ name: kebab }); + expect(el.querySelector("h1")?.textContent?.trim()).toBe( + "Writing System Prompts", + ); }); test("the version list renders each commit's note, author, and when", async () => { diff --git a/apps/web/test/skills-page.test.tsx b/apps/web/test/skills-page.test.tsx index 9bc62315d..98a360f82 100644 --- a/apps/web/test/skills-page.test.tsx +++ b/apps/web/test/skills-page.test.tsx @@ -142,9 +142,52 @@ describe("SkillsPage", () => { [`GET /api/tenants/${TENANT}/skills`]: { skills: [TRIAGE] }, }); const el = await mount(); - expect(el.textContent).toContain("triage"); + // Name slot is a display title, never the raw kebab slug (CL-6747). + expect(el.textContent).toContain("Triage"); expect(el.textContent).toContain("Sorts inbound issues."); expect(el.textContent).toContain("Only me"); + // Badge must not shout via CSS uppercase (CL-6747). + const badge = Array.from(el.querySelectorAll('[data-slot="badge"]')).find( + (node) => node.textContent === "Only me", + ); + expect(badge?.className).toContain("normal-case"); + }); + + test("a kebab skill name is shown title-cased in the Name column", async () => { + stubRoutes({ + ...EMPTY_REGISTRY, + [`GET /api/tenants/${TENANT}/skills`]: { + skills: [ + { + ...TRIAGE, + name: "writing-system-prompts", + description: "Prompt craft.", + }, + ], + }, + }); + const el = await mount(); + expect(el.textContent).toContain("Writing System Prompts"); + const nameCell = Array.from(el.querySelectorAll("td")).find((cell) => + cell.textContent?.includes("Writing System Prompts"), + ); + expect(nameCell?.textContent?.trim()).toBe("Writing System Prompts"); + }); + + test("a shared skill's visibility badge stays sentence case", async () => { + stubRoutes({ + ...EMPTY_REGISTRY, + [`GET /api/tenants/${TENANT}/skills`]: { + skills: [{ ...TRIAGE, scope: "tenant" }], + }, + }); + const el = await mount(); + const badge = Array.from(el.querySelectorAll('[data-slot="badge"]')).find( + (node) => node.textContent === "Everyone", + ); + expect(badge).toBeDefined(); + expect(badge?.className).toContain("normal-case"); + expect(badge?.textContent).toBe("Everyone"); }); test("Create skill posts directly to the registry and opens the new skill's page", async () => { @@ -401,7 +444,7 @@ describe("SkillsPage", () => { }); const el = await mount({ navigate: () => undefined }); const row = Array.from(el.querySelectorAll("tr")).find((tr) => - tr.textContent?.includes("triage"), + tr.textContent?.includes("Triage"), ); await act(async () => { row?.dispatchEvent(new MouseEvent("click", { bubbles: true })); @@ -421,7 +464,7 @@ describe("SkillsPage", () => { const navigated: string[] = []; const el = await mount({ navigate: (to) => navigated.push(to) }); const row = Array.from(el.querySelectorAll("tr")).find((tr) => - tr.textContent?.includes("triage"), + tr.textContent?.includes("Triage"), ); await act(async () => { row?.dispatchEvent(new MouseEvent("click", { bubbles: true })); @@ -439,7 +482,7 @@ describe("CreateSkillDialog validation", () => { ]); }); - test("a name the registry could never carry is rejected before submit", () => { + test("a slug the registry could never carry is rejected before submit", () => { expect( validationIssues({ name: "Summarize Transcript", diff --git a/packages/hub-client/src/default-skills.test.ts b/packages/hub-client/src/default-skills.test.ts new file mode 100644 index 000000000..2dd603d28 --- /dev/null +++ b/packages/hub-client/src/default-skills.test.ts @@ -0,0 +1,19 @@ +import { describe, expect, test } from "bun:test"; + +import { DEFAULT_SKILLS } from "./default-skills"; + +describe("DEFAULT_SKILLS", () => { + test("keeps the writing-system-prompts slug as the registry id", () => { + expect(DEFAULT_SKILLS.map((skill) => skill.name)).toContain( + "writing-system-prompts", + ); + }); + + test("descriptions are person-facing — no lab or intern captions", () => { + for (const skill of DEFAULT_SKILLS) { + const surface = `${skill.description}\n${skill.body}`.toLowerCase(); + expect(surface).not.toMatch(/\blab\b/); + expect(surface).not.toMatch(/\bintern\b/); + } + }); +}); diff --git a/packages/hub-client/src/default-skills.ts b/packages/hub-client/src/default-skills.ts index 58c68a84b..4f42b3a7a 100644 --- a/packages/hub-client/src/default-skills.ts +++ b/packages/hub-client/src/default-skills.ts @@ -13,7 +13,7 @@ export interface DefaultSkill { readonly body: string; } -const WRITING_SYSTEM_PROMPTS_BODY = `Load this before writing or editing any agent's system prompt or a routine's instructions. These rules are how strong lab and product teams write prompts today. +const WRITING_SYSTEM_PROMPTS_BODY = `Load this before writing or editing any agent's system prompt or a routine's instructions. These rules are how strong product teams write prompts today. ## Shape @@ -57,10 +57,9 @@ export const DEFAULT_SKILLS: readonly DefaultSkill[] = [ { name: "writing-system-prompts", description: - "How to write system prompts for agents: structure, behavior " + - "contracts, tool doctrine, and model-fit rules distilled from " + - "current lab guidance. Load before authoring or editing any " + - "agent's prompt.", + "How to write clear system prompts for agents — structure, " + + "behavior, tools, and fitting the prompt to the model. Load " + + "before authoring or editing any agent's prompt.", body: WRITING_SYSTEM_PROMPTS_BODY, }, ]; From c3d891ee5197d33c5de3e352fd2725e2b7cd4c09 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 09:41:46 -0700 Subject: [PATCH 2/3] Match skills chrome tests to the Triage display name --- apps/web/test/stage-chrome-consistency.test.tsx | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/apps/web/test/stage-chrome-consistency.test.tsx b/apps/web/test/stage-chrome-consistency.test.tsx index ebf2bbfef..7f4a8b0b4 100644 --- a/apps/web/test/stage-chrome-consistency.test.tsx +++ b/apps/web/test/stage-chrome-consistency.test.tsx @@ -123,6 +123,6 @@ describe("stage chrome consistency (CL-6368)", () => { }); expect(container.querySelector('[data-slot="table"]')).not.toBeNull(); - expect(container.textContent).toContain("triage"); + expect(container.textContent).toContain("Triage"); }); }); From 5845aa0e4099c41d2d58736584c345ee1663e4a0 Mon Sep 17 00:00:00 2001 From: Sawyer Cutler Date: Mon, 24 Aug 2026 10:20:55 -0700 Subject: [PATCH 3/3] Format files changed in this PR --- apps/web/src/skill-display-name.test.ts | 6 +++--- 1 file changed, 3 insertions(+), 3 deletions(-) diff --git a/apps/web/src/skill-display-name.test.ts b/apps/web/src/skill-display-name.test.ts index a0ae65698..d14d12836 100644 --- a/apps/web/src/skill-display-name.test.ts +++ b/apps/web/src/skill-display-name.test.ts @@ -20,8 +20,8 @@ describe("skillDisplayName", () => { }); test("treats a whitespace-only displayTitle as absent", () => { - expect( - skillDisplayName({ name: "triage", displayTitle: " " }), - ).toBe("Triage"); + expect(skillDisplayName({ name: "triage", displayTitle: " " })).toBe( + "Triage", + ); }); });