Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 2 additions & 1 deletion apps/web/src/pages/skill-detail-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 {
Expand Down Expand Up @@ -324,7 +325,7 @@ export function SkillDetailPage({
<header className="flex flex-wrap items-start justify-between gap-3">
<div className="min-w-0">
<h1 className="truncate text-lg font-semibold tracking-tight">
{skill.name}
{skillDisplayName(skill)}
</h1>
<p className="mt-1 text-xs text-muted-foreground">
Updated {formatRelativeTime(skill.updatedAtIso, now)}
Expand Down
8 changes: 6 additions & 2 deletions apps/web/src/pages/skills-page.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 =
Expand Down Expand Up @@ -244,13 +245,16 @@ export function SkillsPage({
{...rowActivationProps(() => open(skill.name))}
>
<TableCell className="w-48 font-medium">
{skill.name}
{skillDisplayName(skill)}
</TableCell>
<TableCell className="max-w-sm truncate text-muted-foreground">
{skill.description}
</TableCell>
<TableCell className="w-36">
<Badge tone={skill.scope === "tenant" ? "info" : "neutral"}>
<Badge
tone={skill.scope === "tenant" ? "info" : "neutral"}
className="normal-case"
>
{skill.scope === "tenant" ? "Everyone" : "Only me"}
</Badge>
</TableCell>
Expand Down
27 changes: 27 additions & 0 deletions apps/web/src/skill-display-name.test.ts
Original file line number Diff line number Diff line change
@@ -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",
);
});
});
13 changes: 13 additions & 0 deletions apps/web/src/skill-display-name.ts
Original file line number Diff line number Diff line change
@@ -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);
}
28 changes: 25 additions & 3 deletions apps/web/test/skill-detail-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -129,7 +129,9 @@ async function settle() {
});
}

async function mount(): Promise<HTMLDivElement> {
async function mount(
props: { readonly name?: string } = {},
): Promise<HTMLDivElement> {
container = document.createElement("div");
document.body.appendChild(container);
root = createRoot(container);
Expand All @@ -138,14 +140,14 @@ async function mount(): Promise<HTMLDivElement> {
<TestQueryProvider>
<SkillDetailPage
tenantId={TENANT}
name={NAME}
name={props.name ?? NAME}
now={Date.parse("2026-08-05T12:00:00.000Z")}
/>
</TestQueryProvider>,
);
});
await settle();
if (container === null) throw new Error("container went away");
if (container === null) throw new Error("mount left no container");
return container;
}

Expand Down Expand Up @@ -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 () => {
Expand Down
51 changes: 47 additions & 4 deletions apps/web/test/skills-page.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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 () => {
Expand Down Expand Up @@ -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 }));
Expand All @@ -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 }));
Expand All @@ -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",
Expand Down
2 changes: 1 addition & 1 deletion apps/web/test/stage-chrome-consistency.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -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");
});
});
19 changes: 19 additions & 0 deletions packages/hub-client/src/default-skills.test.ts
Original file line number Diff line number Diff line change
@@ -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/);
}
});
});
9 changes: 4 additions & 5 deletions packages/hub-client/src/default-skills.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down Expand Up @@ -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,
},
];
Loading