feat(acp): expose built-in skills through sources list acp calls (#9045)

Signed-off-by: Kalvin Chau <kalvin@block.xyz>
This commit is contained in:
Kalvin C
2026-05-06 07:49:03 -07:00
committed by GitHub
parent 6fc44bf0ed
commit 4ef091a356
17 changed files with 636 additions and 139 deletions
@@ -30,6 +30,7 @@ describe("listSkills", () => {
},
],
})
.mockResolvedValueOnce({ sources: [] })
.mockResolvedValueOnce({
sources: [
{
@@ -58,6 +59,9 @@ describe("listSkills", () => {
type: "skill",
});
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(2, {
type: "builtinSkill",
});
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(3, {
type: "skill",
projectDir: "/tmp/alpha",
});
@@ -84,6 +88,7 @@ describe("listSkills", () => {
it("recognizes legacy .goose project skill paths", async () => {
mockGooseSourcesList
.mockResolvedValueOnce({ sources: [] })
.mockResolvedValueOnce({ sources: [] })
.mockResolvedValueOnce({
sources: [
@@ -133,6 +138,7 @@ describe("listSkills", () => {
},
],
})
.mockResolvedValueOnce({ sources: [] })
.mockRejectedValueOnce(new Error("permission denied"))
.mockResolvedValueOnce({
sources: [
@@ -150,10 +156,129 @@ describe("listSkills", () => {
const { listSkills } = await import("./skills");
const skills = await listSkills(["/tmp/alpha", "/tmp/beta"]);
expect(mockGooseSourcesList).toHaveBeenCalledTimes(3);
expect(mockGooseSourcesList).toHaveBeenCalledTimes(4);
expect(skills.map((skill) => skill.name)).toEqual([
"code-review",
"test-writer",
]);
});
it("keeps filesystem skills when built-in skill listing fails", async () => {
mockGooseSourcesList
.mockResolvedValueOnce({
sources: [
{
type: "skill",
name: "code-review",
description: "Reviews code",
content: "Review carefully",
directory: "/Users/test/.agents/skills/code-review",
global: true,
},
],
})
.mockRejectedValueOnce(new Error("unknown source type"))
.mockResolvedValueOnce({
sources: [
{
type: "skill",
name: "test-writer",
description: "Writes tests",
content: "Write tests",
directory: "/tmp/alpha/.agents/skills/test-writer",
global: false,
},
],
});
const { listSkills } = await import("./skills");
const skills = await listSkills(["/tmp/alpha"]);
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(1, {
type: "skill",
});
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(2, {
type: "builtinSkill",
});
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(3, {
type: "skill",
projectDir: "/tmp/alpha",
});
expect(skills.map((skill) => skill.name)).toEqual([
"code-review",
"test-writer",
]);
});
it("fetches and maps built-in skills without filesystem project/global metadata", async () => {
mockGooseSourcesList
.mockResolvedValueOnce({
sources: [
{
type: "skill",
name: "personal-review",
description: "Reviews personal code",
content: "Review local changes",
directory: "/Users/test/.agents/skills/personal-review",
global: true,
},
],
})
.mockResolvedValueOnce({
sources: [
{
type: "builtinSkill",
name: "goose-doc-guide",
description: "Goose documentation guide",
content: "Use Goose docs",
directory: "builtin://skills/goose-doc-guide",
global: true,
},
],
})
.mockResolvedValueOnce({
sources: [
{
type: "skill",
name: "project-helper",
description: "Helps project work",
content: "Use project context",
directory: "/tmp/alpha/.agents/skills/project-helper",
global: false,
},
],
});
const { listSkills } = await import("./skills");
const skills = await listSkills(["/tmp/alpha"]);
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(1, {
type: "skill",
});
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(2, {
type: "builtinSkill",
});
expect(mockGooseSourcesList).toHaveBeenNthCalledWith(3, {
type: "skill",
projectDir: "/tmp/alpha",
});
expect(skills.map((skill) => skill.name)).toEqual([
"personal-review",
"goose-doc-guide",
"project-helper",
]);
expect(skills).toEqual(
expect.arrayContaining([
expect.objectContaining({
id: "builtin:goose-doc-guide",
name: "goose-doc-guide",
path: "builtin://skills/goose-doc-guide",
fileLocation: "builtin://skills/goose-doc-guide",
sourceKind: "builtin",
sourceLabel: "Built in",
projectLinks: [],
}),
]),
);
});
});
+52 -10
View File
@@ -7,6 +7,7 @@ import {
} from "../lib/skillsPath";
const SKILL_SOURCE_TYPE = "skill" as const;
const BUILTIN_SKILL_SOURCE_TYPE = "builtinSkill" as const;
export interface SkillProjectLink {
id: string;
@@ -14,7 +15,7 @@ export interface SkillProjectLink {
workingDir: string;
}
export type SkillSourceKind = "global" | "project";
export type SkillSourceKind = "global" | "project" | "builtin";
export interface SkillInfo {
id: string;
@@ -33,13 +34,42 @@ export type EditingSkill = Pick<
"name" | "description" | "instructions" | "path" | "fileLocation"
>;
type SkillSourceEntry = SourceEntry & { type: typeof SKILL_SOURCE_TYPE };
type FilesystemSkillSourceEntry = SourceEntry & {
type: typeof SKILL_SOURCE_TYPE;
};
type BuiltinSkillSourceEntry = SourceEntry & {
type: typeof BUILTIN_SKILL_SOURCE_TYPE;
};
type SkillSourceEntry = FilesystemSkillSourceEntry | BuiltinSkillSourceEntry;
function isSkillSource(source: SourceEntry): source is SkillSourceEntry {
function isFilesystemSkillSource(
source: SourceEntry,
): source is FilesystemSkillSourceEntry {
return source.type === SKILL_SOURCE_TYPE;
}
function isSkillSource(source: SourceEntry): source is SkillSourceEntry {
return (
source.type === SKILL_SOURCE_TYPE ||
source.type === BUILTIN_SKILL_SOURCE_TYPE
);
}
function toSkillInfo(source: SkillSourceEntry): SkillInfo {
if (source.type === BUILTIN_SKILL_SOURCE_TYPE) {
return {
id: `builtin:${source.name}`,
name: source.name,
description: source.description,
instructions: source.content,
path: source.directory,
fileLocation: source.directory,
sourceKind: "builtin",
sourceLabel: "Built in",
projectLinks: [],
};
}
const sourceKind: SkillSourceKind = source.global ? "global" : "project";
const projectRoot = source.global
? null
@@ -93,20 +123,29 @@ export async function listSkills(
projectDirs: string[] = [],
): Promise<SkillInfo[]> {
const client = await getClient();
const fetchSources = (projectDir?: string) =>
const fetchSources = (
type: typeof SKILL_SOURCE_TYPE | typeof BUILTIN_SKILL_SOURCE_TYPE,
projectDir?: string,
) =>
client.goose.GooseSourcesList({
type: SKILL_SOURCE_TYPE,
type,
...(projectDir ? { projectDir } : {}),
});
const globalResponse = await fetchSources();
const [globalResponse, builtinResponse] = await Promise.all([
fetchSources(SKILL_SOURCE_TYPE),
fetchSources(BUILTIN_SKILL_SOURCE_TYPE).catch(() => null),
]);
const projectResponses = await Promise.allSettled(
uniqueProjectDirs(projectDirs).map((projectDir) =>
fetchSources(projectDir),
fetchSources(SKILL_SOURCE_TYPE, projectDir),
),
);
const responses = [
{ response: globalResponse, projectResponse: false },
...(builtinResponse
? [{ response: builtinResponse, projectResponse: false }]
: []),
...projectResponses.flatMap((result) =>
result.status === "fulfilled"
? [{ response: result.value, projectResponse: true }]
@@ -122,7 +161,10 @@ export async function listSkills(
continue;
}
const key = `${source.global ? "global" : "project"}:${source.directory}`;
const key =
source.type === BUILTIN_SKILL_SOURCE_TYPE
? `builtin:${source.name}`
: `${source.global ? "global" : "project"}:${source.directory}`;
if (seen.has(key)) {
continue;
}
@@ -158,7 +200,7 @@ export async function updateSkill(
content: instructions,
});
if (!isSkillSource(response.source)) {
if (!isFilesystemSkillSource(response.source)) {
throw new Error(`Unexpected source type returned: ${response.source.type}`);
}
@@ -191,5 +233,5 @@ export async function importSkills(
global: true,
});
return response.sources.filter(isSkillSource).map(toSkillInfo);
return response.sources.filter(isFilesystemSkillSource).map(toSkillInfo);
}
@@ -8,6 +8,10 @@ export function useSkillImportExport(onAfterImport: () => Promise<void>) {
const { t } = useTranslation(["skills"]);
const handleExport = async (skill: SkillInfo) => {
if (skill.sourceKind === "builtin") {
return;
}
try {
const result = await exportSkill(skill.path);
downloadExport(result.json, result.filename);
@@ -1,6 +1,6 @@
import type { SkillInfo } from "../api/skills";
export type SkillsFilter = "all" | "global" | `project:${string}`;
export type SkillsFilter = "all" | "global" | "builtin" | `project:${string}`;
export interface SkillsSection {
id: string;
@@ -77,9 +77,11 @@ export function filterSkills(
? true
: filters.activeFilter === "global"
? skill.sourceKind === "global"
: skill.projectLinks.some(
(project) => `project:${project.id}` === filters.activeFilter,
);
: filters.activeFilter === "builtin"
? skill.sourceKind === "builtin"
: skill.projectLinks.some(
(project) => `project:${project.id}` === filters.activeFilter,
);
return matchesSearch && matchesFilter;
});
@@ -89,7 +91,11 @@ export function groupSkills(
filteredSkills: SkillInfo[],
activeFilter: SkillsFilter,
projectFilters: { id: string; name: string }[],
labels: { personalTitle: string; projectsFallback: string },
labels: {
personalTitle: string;
builtinTitle: string;
projectsFallback: string;
},
): SkillsSection[] {
if (activeFilter === "global") {
return [
@@ -101,6 +107,16 @@ export function groupSkills(
];
}
if (activeFilter === "builtin") {
return [
{
id: "builtin",
title: labels.builtinTitle,
skills: [...filteredSkills].sort(compareSkillsByName),
},
];
}
if (activeFilter.startsWith("project:")) {
const projectId = activeFilter.slice("project:".length);
const projectName =
@@ -120,6 +136,10 @@ export function groupSkills(
.filter((skill) => skill.sourceKind === "global")
.sort(compareSkillsByName);
const builtinSkills = filteredSkills
.filter((skill) => skill.sourceKind === "builtin")
.sort(compareSkillsByName);
const projectSections = projectFilters
.map((project) => ({
id: `project:${project.id}`,
@@ -142,6 +162,15 @@ export function groupSkills(
},
]
: []),
...(builtinSkills.length > 0
? [
{
id: "builtin",
title: labels.builtinTitle,
skills: builtinSkills,
},
]
: []),
...projectSections,
];
}
@@ -97,6 +97,61 @@ export function SkillDetailPage({
const editLabel = t("common:actions.edit");
const revealLabel = t("view.reveal");
const moreLabel = t("view.more");
const isBuiltin = skill.sourceKind === "builtin";
const actions = (
<>
{onStartChat ? (
<SkillHeaderActionButton
label={startChatLabel}
icon={<IconMessagePlus className="size-3.5" />}
tooltipSide="top"
onClick={() => onStartChat(skill)}
/>
) : null}
{!isBuiltin ? (
<>
<SkillHeaderActionButton
label={editLabel}
icon={<IconPencil className="size-3.5" />}
tooltipSide="top"
onClick={() => onEdit(skill)}
/>
<SkillHeaderActionButton
label={revealLabel}
icon={<IconFolderOpen className="size-3.5" />}
tooltipSide="top"
onClick={() => onReveal(skill)}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
size="icon-xs"
variant="outline-flat"
aria-label={moreLabel}
>
<IconDots className="size-3.5" />
<span className="sr-only">{moreLabel}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8}>
<DropdownMenuItem onSelect={() => onShare(skill)}>
<IconShare className="size-3.5" />
{t("view.share")}
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => onDelete(skill)}
>
<IconTrash className="size-3.5" />
{t("common:actions.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
) : null}
</>
);
return (
<DetailPageShell>
@@ -117,111 +172,71 @@ export function SkillDetailPage({
description={skill.description}
actionsPlacement="below"
descriptionClassName="max-w-3xl leading-relaxed"
actions={
<>
{onStartChat ? (
<SkillHeaderActionButton
label={startChatLabel}
icon={<IconMessagePlus className="size-3.5" />}
tooltipSide="top"
onClick={() => onStartChat(skill)}
/>
) : null}
<SkillHeaderActionButton
label={editLabel}
icon={<IconPencil className="size-3.5" />}
tooltipSide="top"
onClick={() => onEdit(skill)}
/>
<SkillHeaderActionButton
label={revealLabel}
icon={<IconFolderOpen className="size-3.5" />}
tooltipSide="top"
onClick={() => onReveal(skill)}
/>
<DropdownMenu>
<DropdownMenuTrigger asChild>
<Button
type="button"
size="icon-xs"
variant="outline-flat"
aria-label={moreLabel}
>
<IconDots className="size-3.5" />
<span className="sr-only">{moreLabel}</span>
</Button>
</DropdownMenuTrigger>
<DropdownMenuContent align="end" sideOffset={8}>
<DropdownMenuItem onSelect={() => onShare(skill)}>
<IconShare className="size-3.5" />
{t("view.share")}
</DropdownMenuItem>
<DropdownMenuItem
variant="destructive"
onSelect={() => onDelete(skill)}
>
<IconTrash className="size-3.5" />
{t("common:actions.delete")}
</DropdownMenuItem>
</DropdownMenuContent>
</DropdownMenu>
</>
}
actions={actions}
actionsClassName="gap-2"
/>
</div>
<PageColumns
defaultSidebarSize={28}
minSidebarSize={22}
maxSidebarSize={36}
minContentSize={52}
sidebar={
<aside className="space-y-5">
<section className="space-y-5 border-b border-border pb-5">
<DetailField
label={t("view.source")}
contentClassName="space-y-1 text-foreground"
>
{sourceLabels.map((label) => (
<p key={label}>{label}</p>
))}
</DetailField>
{skill.projectLinks.length > 0 ? (
<DetailField
label={t("view.projects")}
contentClassName="space-y-1.5"
>
{skill.projectLinks.map((project) => (
<div key={`${project.id}-${project.workingDir}`}>
<p>{project.name}</p>
<p className="text-xs text-muted-foreground">
{project.workingDir}
</p>
</div>
))}
</DetailField>
) : null}
<DetailField
label={t("view.location")}
contentAs="p"
contentClassName="break-all text-foreground"
>
{skill.fileLocation}
</DetailField>
</section>
</aside>
}
>
{isBuiltin ? (
<section className="space-y-4 pb-6">
<DetailField label={t("view.instructions")} />
<MessageResponse className="min-w-0 text-sm leading-6">
{skill.instructions || " "}
</MessageResponse>
</section>
</PageColumns>
) : (
<PageColumns
defaultSidebarSize={28}
minSidebarSize={22}
maxSidebarSize={36}
minContentSize={52}
sidebar={
<aside className="space-y-5">
<section className="space-y-5 border-b border-border pb-5">
<DetailField
label={t("view.source")}
contentClassName="space-y-1 text-foreground"
>
{sourceLabels.map((label) => (
<p key={label}>{label}</p>
))}
</DetailField>
{skill.projectLinks.length > 0 ? (
<DetailField
label={t("view.projects")}
contentClassName="space-y-1.5"
>
{skill.projectLinks.map((project) => (
<div key={`${project.id}-${project.workingDir}`}>
<p>{project.name}</p>
<p className="text-xs text-muted-foreground">
{project.workingDir}
</p>
</div>
))}
</DetailField>
) : null}
<DetailField
label={t("view.location")}
contentAs="p"
contentClassName="break-all text-foreground"
>
{skill.fileLocation}
</DetailField>
</section>
</aside>
}
>
<section className="space-y-4 pb-6">
<DetailField label={t("view.instructions")} />
<MessageResponse className="min-w-0 text-sm leading-6">
{skill.instructions || " "}
</MessageResponse>
</section>
</PageColumns>
)}
</DetailPageShell>
);
}
@@ -10,6 +10,7 @@ interface SkillsToolbarProps {
onSearchChange: (value: string) => void;
activeFilter: SkillsFilter;
onActiveFilterChange: (filter: SkillsFilter) => void;
hasBuiltinSkills: boolean;
projectFilters: { id: string; name: string }[];
dropHandlers?: React.HTMLAttributes<HTMLDivElement>;
isDragOver?: boolean;
@@ -41,6 +42,7 @@ export function SkillsToolbar({
onSearchChange,
activeFilter,
onActiveFilterChange,
hasBuiltinSkills,
projectFilters,
dropHandlers,
isDragOver,
@@ -74,6 +76,14 @@ export function SkillsToolbar({
>
{t("view.filtersGlobal")}
</FilterButton>
{hasBuiltinSkills ? (
<FilterButton
active={activeFilter === "builtin"}
onClick={() => onActiveFilterChange("builtin")}
>
{t("view.filtersBuiltin")}
</FilterButton>
) : null}
{projectFilters.map((project) => {
const filterValue = `project:${project.id}` as const;
return (
@@ -78,6 +78,10 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
}, [loadSkills]);
const projectFilters = useMemo(() => uniqueProjectFilters(skills), [skills]);
const hasBuiltinSkills = useMemo(
() => skills.some((skill) => skill.sourceKind === "builtin"),
[skills],
);
useEffect(() => {
if (!activeFilter.startsWith("project:")) {
@@ -90,6 +94,12 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
}
}, [activeFilter, projectFilters]);
useEffect(() => {
if (activeFilter === "builtin" && !hasBuiltinSkills) {
setActiveFilter("all");
}
}, [activeFilter, hasBuiltinSkills]);
const filteredSkills = useMemo(
() => filterSkills(skills, { search, activeFilter }),
[skills, search, activeFilter],
@@ -99,6 +109,7 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
() =>
groupSkills(filteredSkills, activeFilter, projectFilters, {
personalTitle: t("view.filtersGlobal"),
builtinTitle: t("view.filtersBuiltin"),
projectsFallback: t("view.projects"),
}),
[filteredSkills, activeFilter, projectFilters, t],
@@ -117,11 +128,18 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
skills.find((skill) => skill.id === activeSkillId) ?? null;
const handleDelete = (skill: SkillInfo) => {
if (skill.sourceKind === "builtin") {
return;
}
setDeletingSkill(skill);
};
const handleConfirmDeleteSkill = async () => {
if (!deletingSkill) return;
if (deletingSkill.sourceKind === "builtin") {
setDeletingSkill(null);
return;
}
try {
await deleteSkill(deletingSkill.path);
await loadSkills();
@@ -136,6 +154,9 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
};
const handleEdit = (skill: SkillInfo) => {
if (skill.sourceKind === "builtin") {
return;
}
setEditingSkill({
name: skill.name,
description: skill.description,
@@ -147,6 +168,9 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
};
const handleReveal = useCallback((skill: SkillInfo) => {
if (skill.sourceKind === "builtin") {
return;
}
void revealInFileManager(skill.path);
}, []);
@@ -193,6 +217,16 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
handleExport,
} = useSkillImportExport(refreshSkills);
const handleShare = useCallback(
(skill: SkillInfo) => {
if (skill.sourceKind === "builtin") {
return;
}
void handleExport(skill);
},
[handleExport],
);
const handleSelectSkill = (skill: SkillInfo) => {
setActiveSkillId(skill.id);
};
@@ -217,7 +251,7 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
onBack={() => setActiveSkillId(null)}
onEdit={handleEdit}
onReveal={handleReveal}
onShare={handleExport}
onShare={handleShare}
onStartChat={onStartChatWithSkill ? handleStartChat : undefined}
onDelete={handleDelete}
/>
@@ -261,6 +295,7 @@ export function SkillsView({ onStartChatWithSkill }: SkillsViewProps) {
onSearchChange={setSearch}
activeFilter={activeFilter}
onActiveFilterChange={setActiveFilter}
hasBuiltinSkills={hasBuiltinSkills}
projectFilters={projectFilters}
dropHandlers={dropHandlers}
isDragOver={isDragOver}
@@ -60,6 +60,18 @@ const mockSkills: SkillInfo[] = [
},
];
const builtinSkill: SkillInfo = {
id: "builtin:goose-doc-guide",
name: "goose-doc-guide",
description: "Reference Goose documentation",
instructions: "Fetch Goose docs before answering.",
path: "builtin://skills/goose-doc-guide",
fileLocation: "builtin://skills/goose-doc-guide",
sourceKind: "builtin" as const,
sourceLabel: "Built in",
projectLinks: [],
};
vi.mock("../../api/skills", () => ({
listSkills: vi.fn().mockResolvedValue([]),
createSkill: vi.fn().mockResolvedValue(undefined),
@@ -87,12 +99,13 @@ vi.mock("@/features/projects/stores/projectStore", () => ({
) => selector({ projects: mockProjects }),
}));
const { listSkills, deleteSkill, updateSkill } = (await import(
const { listSkills, deleteSkill, updateSkill, exportSkill } = (await import(
"../../api/skills"
)) as unknown as {
listSkills: ReturnType<typeof vi.fn>;
deleteSkill: ReturnType<typeof vi.fn>;
updateSkill: ReturnType<typeof vi.fn>;
exportSkill: ReturnType<typeof vi.fn>;
};
beforeEach(() => {
@@ -377,6 +390,31 @@ describe("SkillsView", () => {
expect(screen.getByText("test-writer")).toBeInTheDocument();
});
it("groups and filters built-in skills separately", async () => {
listSkills.mockResolvedValue([...mockSkills, builtinSkill]);
const user = userEvent.setup();
render(<SkillsView />);
await screen.findByText("goose-doc-guide");
expect(
screen.getByRole("button", { name: "Built in" }),
).toBeInTheDocument();
const sectionButtons = screen
.getAllByRole("button")
.filter((button) => /\d skill/.test(button.textContent ?? ""));
expect(sectionButtons.map((button) => button.textContent)).toEqual([
expect.stringContaining("Personal"),
expect.stringContaining("Built in"),
expect.stringContaining("alpha"),
]);
await user.click(screen.getByRole("button", { name: "Built in" }));
expect(screen.getByText("goose-doc-guide")).toBeInTheDocument();
expect(screen.queryByText("code-review")).not.toBeInTheDocument();
expect(screen.queryByText("test-writer")).not.toBeInTheDocument();
});
it("shows a delete confirmation from the detail panel", async () => {
listSkills.mockResolvedValue(mockSkills);
const user = userEvent.setup();
@@ -403,6 +441,46 @@ describe("SkillsView", () => {
});
});
it("shows built-in details without filesystem actions and still starts chat", async () => {
listSkills.mockResolvedValue([...mockSkills, builtinSkill]);
const onStartChatWithSkill = vi.fn();
const user = userEvent.setup();
render(<SkillsView onStartChatWithSkill={onStartChatWithSkill} />);
await screen.findByText("goose-doc-guide");
await user.click(
screen.getByRole("button", { name: "Open goose-doc-guide details" }),
);
expect(
screen.getByText("Fetch Goose docs before answering."),
).toBeInTheDocument();
expect(screen.queryByText("Location")).not.toBeInTheDocument();
expect(
screen.queryByText("builtin://skills/goose-doc-guide"),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Edit" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "Show in folder" }),
).not.toBeInTheDocument();
expect(
screen.queryByRole("button", { name: "More" }),
).not.toBeInTheDocument();
await user.click(screen.getByRole("button", { name: "Start chat" }));
expect(onStartChatWithSkill).toHaveBeenCalledWith(
expect.objectContaining({ name: "goose-doc-guide" }),
null,
);
expect(updateSkill).not.toHaveBeenCalled();
expect(deleteSkill).not.toHaveBeenCalled();
expect(exportSkill).not.toHaveBeenCalled();
});
it("passes saved project working directories into listSkills", async () => {
mockProjects = [
{
@@ -30,6 +30,7 @@
"exportedTo": "Exported to {{filename}}",
"filePath": "File path",
"filtersAllSources": "All",
"filtersBuiltin": "Built in",
"filtersGlobal": "Personal",
"importError": "Failed to import skill",
"importSuccess": "Skill imported",
@@ -30,6 +30,7 @@
"exportedTo": "Exportado a {{filename}}",
"filePath": "Ruta del archivo",
"filtersAllSources": "Todas",
"filtersBuiltin": "Integradas",
"filtersGlobal": "Personal",
"importError": "Error al importar la skill",
"importSuccess": "Skill importada",
+9 -6
View File
@@ -764,8 +764,9 @@ export type CreateSourceResponse = {
};
/**
* A source discovered by Goose and backed by an on-disk path. Sources may be
* either `global` (shared across all projects) or project-specific.
* A source discovered by Goose. Filesystem sources use an on-disk path;
* built-in sources use a stable synthetic path. Sources may be either
* `global` (shared across all projects) or project-specific.
*/
export type SourceEntry = {
type: SourceType;
@@ -774,7 +775,8 @@ export type SourceEntry = {
content: string;
/**
* Absolute path to the source on disk. A directory for skills, a file for
* recipes and agents.
* recipes and agents. Built-in skills use read-only synthetic
* `builtin://skills/<name>` paths.
*/
directory: string;
/**
@@ -792,9 +794,10 @@ export type SourceEntry = {
/**
* List discovered sources.
*
* Today this endpoint only returns skills. If `type` is omitted, it defaults
* to listing skill sources. Both global and project-scoped skills are included
* when `project_dir` is set.
* If `type` is omitted or `skill`, this lists filesystem/plugin skills only.
* Both global and project-scoped skills are included when `project_dir` is
* set. If `type` is `builtinSkill`, this lists shipped read-only built-in
* skills.
*/
export type ListSourcesRequest = {
type?: SourceType | null;
+7 -5
View File
@@ -774,8 +774,9 @@ export const zCreateSourceRequest = z.object({
});
/**
* A source discovered by Goose and backed by an on-disk path. Sources may be
* either `global` (shared across all projects) or project-specific.
* A source discovered by Goose. Filesystem sources use an on-disk path;
* built-in sources use a stable synthetic path. Sources may be either
* `global` (shared across all projects) or project-specific.
*/
export const zSourceEntry = z.object({
type: zSourceType,
@@ -794,9 +795,10 @@ export const zCreateSourceResponse = z.object({
/**
* List discovered sources.
*
* Today this endpoint only returns skills. If `type` is omitted, it defaults
* to listing skill sources. Both global and project-scoped skills are included
* when `project_dir` is set.
* If `type` is omitted or `skill`, this lists filesystem/plugin skills only.
* Both global and project-scoped skills are included when `project_dir` is
* set. If `type` is `builtinSkill`, this lists shipped read-only built-in
* skills.
*/
export const zListSourcesRequest = z.object({
type: z.union([