redesign skills library (#8868)

Signed-off-by: morgmart <98432065+morgmart@users.noreply.github.com>
This commit is contained in:
morgmart
2026-04-27 23:58:17 -07:00
committed by GitHub
parent 96fa25bce8
commit b35eaf4bf9
33 changed files with 2879 additions and 767 deletions
+11 -1
View File
@@ -75,18 +75,28 @@ export const MOCK_PROJECTS = [
];
export const MOCK_SKILLS = [
{
name: "layout",
description: "Improves layout, spacing, and visual hierarchy",
instructions:
"When asked to improve a UI layout, tighten spacing, strengthen hierarchy, and refine composition.",
path: "/mock/.agents/skills/layout/SKILL.md",
global: true,
},
{
name: "code-review",
description: "Reviews code for quality and best practices",
instructions:
"When asked to review code, analyze the diff and provide feedback on code quality, potential bugs, and best practices.",
path: "/mock/.agents/skills/code-review/SKILL.md",
global: true,
},
{
name: "test-writer",
description: "Generates unit tests for given code",
instructions:
"When asked to write tests, generate comprehensive unit tests covering edge cases, happy paths, and error scenarios.",
path: "/mock/.agents/skills/test-writer/SKILL.md",
path: "/tmp/alpha/.goose/skills/test-writer/SKILL.md",
global: false,
},
];
+7 -5
View File
@@ -95,7 +95,7 @@ export function buildInitScript(options?: {
description: s.description,
content: s.instructions ?? s.content ?? "",
directory: (s.path ?? ("/mock/.agents/skills/" + s.name + "/SKILL.md")).replace(/\\/SKILL\\.md$/, ""),
global: true,
global: s.global ?? true,
supportingFiles: [],
});
@@ -196,10 +196,10 @@ export function buildInitScript(options?: {
content: message.params?.content ?? "",
directory: "/mock/.agents/skills/" + (message.params?.name ?? "new-skill"),
global: message.params?.global ?? true,
supportingFiles: [],
},
});
case "_goose/sources/update": {
case "_goose/sources/update":
case "goose/sources/update": {
const path = message.params?.path ?? "/mock/.agents/skills/updated-skill";
const nextName = message.params?.name;
const name =
@@ -218,14 +218,16 @@ export function buildInitScript(options?: {
description: message.params?.description ?? "",
content: message.params?.content ?? "",
directory,
global: true,
global: message.params?.global ?? true,
supportingFiles: [],
},
});
}
case "_goose/sources/delete":
case "goose/sources/delete":
return jsonRpcResult(message.id, {});
case "_goose/sources/export": {
case "_goose/sources/export":
case "goose/sources/export": {
const path = message.params?.path ?? "/mock/.agents/skills/skill";
const name = String(path).split("/").filter(Boolean).at(-1) ?? "skill";
return jsonRpcResult(message.id, {
+91 -220
View File
@@ -6,254 +6,125 @@ import {
} from "./fixtures/tauri-mock";
test.describe("Skills view", () => {
test("navigates to skills view from sidebar", async ({
test("navigates to skills view and shows the redesigned header", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await expect(page.locator("h1", { hasText: "Skills" })).toBeVisible();
await expect(
page.getByText("Reusable instructions for your AI agents"),
).toBeVisible();
});
test("displays skills from mock data", async ({ tauriMocked: page }) => {
await navigateToSkills(page);
await expect(page.getByText("code-review")).toBeVisible();
await expect(page.getByText("test-writer")).toBeVisible();
await expect(
page.getByText("Reviews code for quality and best practices"),
).toBeVisible();
await expect(
page.getByText("Generates unit tests for given code"),
).toBeVisible();
});
test("shows New Skill and Import buttons", async ({ tauriMocked: page }) => {
await navigateToSkills(page);
// The header has "New Skill" and "Import" buttons
await expect(
page.getByRole("button", { name: "New Skill" }).first(),
page.getByText(/Skills are reusable instructions/),
).toBeVisible();
await expect(page.getByRole("button", { name: "Import" })).toBeVisible();
await expect(page.getByRole("button", { name: "New Skill" })).toBeVisible();
});
test("opens create skill dialog from header button", async ({
test("shows skills in the list and opens a dedicated detail page", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
// Click the first "New Skill" button (in the header)
await page.getByRole("button", { name: "New Skill" }).first().click();
await expect(
page.getByRole("button", { name: "Open layout details" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Open code-review details" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Open test-writer details" }),
).toBeVisible();
await page
.getByRole("button", { name: "Open test-writer details" })
.click();
await expect(
page.getByRole("button", { name: "Back to skills" }),
).toBeVisible();
await expect(page.getByText("alpha").first()).toBeVisible();
await expect(page.getByText("Quality")).toBeVisible();
await expect(
page.getByText("/tmp/alpha/.goose/skills/test-writer/SKILL.md"),
).toBeVisible();
});
test("category filtering isolates inferred groups", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "Filter by category" }).click();
await page.getByRole("menuitemcheckbox", { name: "Design" }).click();
await page.keyboard.press("Escape");
await expect(
page.getByRole("button", { name: "Open layout details" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Open code-review details" }),
).not.toBeVisible();
await expect(
page.getByRole("button", { name: "Open test-writer details" }),
).not.toBeVisible();
});
test("search filters the list", async ({ tauriMocked: page }) => {
await navigateToSkills(page);
await page.getByPlaceholder("Search skills").fill("review");
await expect(page.getByText("code-review")).toBeVisible();
await expect(page.getByText("test-writer")).not.toBeVisible();
});
test("project filtering isolates project skills", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page
.getByRole("main")
.getByRole("button", { name: "Alpha", exact: true })
.click();
await expect(
page.getByRole("button", { name: "Open test-writer details" }),
).toBeVisible();
await expect(
page.getByRole("button", { name: "Open code-review details" }),
).not.toBeVisible();
});
test("opens the create skill dialog", async ({ tauriMocked: page }) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "New Skill" }).click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await expect(dialog.locator("h2", { hasText: "New Skill" })).toBeVisible();
// Check form fields
await expect(dialog.getByPlaceholder("my-skill-name")).toBeVisible();
await expect(
dialog.getByPlaceholder("What it does and when to use it..."),
).toBeVisible();
await expect(
dialog.getByPlaceholder("Markdown instructions the agent will follow..."),
).toBeVisible();
});
test("create skill dialog has disabled Create Skill button when empty", async ({
test("shows the empty state when no skills are available", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "New Skill" }).first().click();
const dialog = page.getByRole("dialog");
await expect(
dialog.getByRole("button", { name: "Create Skill" }),
).toBeDisabled();
});
test("create skill dialog enables Create Skill when name and description filled", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "New Skill" }).first().click();
const dialog = page.getByRole("dialog");
await dialog.getByPlaceholder("my-skill-name").fill("my-new-skill");
await dialog
.getByPlaceholder("What it does and when to use it...")
.fill("A test skill");
await expect(
dialog.getByRole("button", { name: "Create Skill" }),
).toBeEnabled();
});
test("skill name auto-formats to kebab-case", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "New Skill" }).first().click();
const dialog = page.getByRole("dialog");
const nameInput = dialog.getByPlaceholder("my-skill-name");
// Type mixed case with spaces — should auto-format
await nameInput.fill("My Skill Name");
// The handleNameChange function lowercases and replaces non-alphanumeric with hyphens
await expect(nameInput).toHaveValue("my-skill-name");
});
test("shows validation error for trailing hyphen", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "New Skill" }).first().click();
const dialog = page.getByRole("dialog");
await dialog.getByPlaceholder("my-skill-name").pressSequentially("test ");
await expect(
dialog.getByText(
"Use 164 lowercase letters, numbers, or hyphens. Names cannot start or end with a hyphen.",
),
).toBeVisible();
});
test("closes skill dialog via Close button", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "New Skill" }).first().click();
await expect(page.getByRole("dialog")).toBeVisible();
await page.getByRole("button", { name: "Close" }).click();
await expect(page.getByRole("dialog")).not.toBeVisible();
});
test("closes skill dialog via Cancel button", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByRole("button", { name: "New Skill" }).first().click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await dialog.getByRole("button", { name: "Cancel" }).click();
await expect(page.getByRole("dialog")).not.toBeVisible();
});
test("skill options menu shows correct items", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByLabel("Options for code-review").click();
const menu = page.getByRole("menu");
await expect(menu).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Edit" })).toBeVisible();
await expect(
menu.getByRole("menuitem", { name: "Duplicate" }),
).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Export" })).toBeVisible();
await expect(menu.getByRole("menuitem", { name: "Delete" })).toBeVisible();
});
test("Edit opens edit dialog with pre-filled editable fields", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByLabel("Options for code-review").click();
await page.getByRole("menuitem", { name: "Edit" }).click();
const dialog = page.getByRole("dialog");
await expect(dialog).toBeVisible();
await expect(dialog.locator("h2", { hasText: "Edit Skill" })).toBeVisible();
const nameInput = dialog.getByPlaceholder("my-skill-name");
const descriptionInput = dialog.getByPlaceholder(
"What it does and when to use it...",
);
const instructionsInput = dialog.getByPlaceholder(
"Markdown instructions the agent will follow...",
);
await expect(nameInput).toHaveValue("code-review");
await expect(descriptionInput).toHaveValue(
"Reviews code for quality and best practices",
);
await expect(instructionsInput).toHaveValue(
"When asked to review code, analyze the diff and provide feedback on code quality, potential bugs, and best practices.",
);
await expect(
dialog.getByText(
"Path on disk: /mock/.agents/skills/code-review/SKILL.md",
),
).toBeVisible();
await nameInput.fill("renamed-skill");
await expect(nameInput).toHaveValue("renamed-skill");
await expect(
dialog.getByText(
"Path on disk: /mock/.agents/skills/renamed-skill/SKILL.md",
),
).toBeVisible();
});
test("Delete triggers confirmation dialog", async ({ tauriMocked: page }) => {
await navigateToSkills(page);
await page.getByLabel("Options for code-review").click();
await page.getByRole("menuitem", { name: "Delete" }).click();
await expect(page.getByText("Delete skill?")).toBeVisible();
await expect(
page.getByText(/Are you sure you want to delete.*code-review/),
).toBeVisible();
});
test("Cancel in delete confirmation closes dialog", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page.getByLabel("Options for code-review").click();
await page.getByRole("menuitem", { name: "Delete" }).click();
await expect(page.getByText("Delete skill?")).toBeVisible();
// Find the Cancel button within the delete confirmation container
const confirmDialog = page.locator(".max-w-sm", {
has: page.getByText("Delete skill?"),
});
await confirmDialog.getByRole("button", { name: "Cancel" }).click();
await expect(page.getByText("Delete skill?")).not.toBeVisible();
// Skill should still be listed
await expect(page.getByText("code-review")).toBeVisible();
});
test("search filters skills", async ({ tauriMocked: page }) => {
await navigateToSkills(page);
await page
.getByPlaceholder("Search skills by name or description...")
.fill("review");
await expect(page.getByText("code-review")).toBeVisible();
await expect(page.getByText("test-writer")).not.toBeVisible();
// Clear search
await page
.getByPlaceholder("Search skills by name or description...")
.clear();
await expect(page.getByText("code-review")).toBeVisible();
await expect(page.getByText("test-writer")).toBeVisible();
});
test("search with no results shows empty state", async ({
tauriMocked: page,
}) => {
await navigateToSkills(page);
await page
.getByPlaceholder("Search skills by name or description...")
.fill("nonexistent-xyz");
await expect(page.getByText("No matching skills")).toBeVisible();
await expect(page.getByText("Try a different search term.")).toBeVisible();
});
test("empty skills state shows create prompt", async ({
tauriMocked: page,
}) => {
// Override with empty skills — must be called BEFORE navigateToSkills
await page.addInitScript({
content: buildInitScript({ personas: [], skills: [] }),
content: buildInitScript({ personas: [], projects: [], skills: [] }),
});
await navigateToSkills(page);
await expect(page.getByText("No skills yet")).toBeVisible();
await expect(
page.getByText("Create a skill or drop a .skill.json file here."),
).toBeVisible();
// New Skill button in empty state
await expect(
page.getByRole("button", { name: "New Skill" }).first(),
page.getByText("Create a skill or import one to get started."),
).toBeVisible();
await expect(page.getByRole("button", { name: "New Skill" })).toHaveCount(
2,
);
await expect(page.getByRole("button", { name: "Import" })).toHaveCount(2);
});
});