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
63 changes: 62 additions & 1 deletion apps/app/src/components/tools/SkillsCollection.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -34,6 +34,7 @@ import {
} from "@/lib/provider-icon";

type ResourceProviderFilter = "bb" | SkillProvider;
type ResourceSkillSourceFilter = "included" | "bb-official";
type ResourceSortMode = "provider" | "alpha";
type ResourceSortDirection = "asc" | "desc";

Expand All @@ -43,6 +44,11 @@ const RESOURCE_PROVIDER_FILTERS: readonly ResourceProviderFilter[] = [
"codex",
];

const RESOURCE_SKILL_SOURCE_FILTERS: readonly ResourceSkillSourceFilter[] = [
"included",
"bb-official",
];

function providerLabel(provider: SkillProvider | null): string {
return provider === null
? "bb"
Expand All @@ -57,6 +63,24 @@ function providerFilterLabel(provider: ResourceProviderFilter): string {
return provider === "bb" ? "bb" : providerLabel(provider);
}

function skillSourceFilterId(
skill: SkillSummary,
): ResourceSkillSourceFilter | null {
if (skill.scope === "bb-builtin") return "bb-official";
if (skill.scope === "plugin") return "included";
return null;
}

function skillSourceFilterLabel(source: ResourceSkillSourceFilter): string {
return source === "bb-official" ? "BB official" : "Included";
}

function isResourceSkillSourceFilter(
value: string,
): value is ResourceSkillSourceFilter {
return value === "included" || value === "bb-official";
}

export function ProviderLogo({
providerId,
className,
Expand Down Expand Up @@ -223,6 +247,9 @@ export function SkillsOverview({
const [providerFilters, setProviderFilters] = useState<
ResourceProviderFilter[]
>(["bb"]);
const [sourceFilters, setSourceFilters] = useState<
ResourceSkillSourceFilter[]
>(["bb-official"]);
const [sortMode, setSortMode] = useState<ResourceSortMode>("alpha");
const [sortDirection, setSortDirection] =
useState<ResourceSortDirection>("asc");
Expand Down Expand Up @@ -254,6 +281,14 @@ export function SkillsOverview({
!providerCounts.has(provider) && !providerFilters.includes(provider),
}));
}, [providerCounts, providerFilters]);
const sourceOptions = useMemo(
() =>
RESOURCE_SKILL_SOURCE_FILTERS.map((source) => ({
id: source,
label: skillSourceFilterLabel(source),
})),
[],
);
useEffect(() => {
if (sortMode === "provider" && providerBucketCount <= 1) {
setSortMode("alpha");
Expand All @@ -262,6 +297,10 @@ export function SkillsOverview({
}, [providerBucketCount, sortMode]);
const visibleSkills = useMemo(() => {
const filtered = skills.filter((skill) => {
const source = skillSourceFilterId(skill);
if (source !== null && !sourceFilters.includes(source)) {
return false;
}
if (
providerFilters.length > 0 &&
!providerFilters.includes(skillProviderFilterId(skill))
Expand Down Expand Up @@ -297,12 +336,20 @@ export function SkillsOverview({
if (base !== 0) return sortDirection === "asc" ? base : -base;
return left.filePath.localeCompare(right.filePath);
});
}, [normalizedQuery, providerFilters, skills, sortDirection, sortMode]);
}, [
normalizedQuery,
providerFilters,
skills,
sortDirection,
sortMode,
sourceFilters,
]);
const libraryPagination = useResourcePagination(visibleSkills, {
pageSize: libraryPageSize,
resetKey: [
normalizedQuery,
providerFilters.join(","),
sourceFilters.join(","),
sortMode,
sortDirection,
].join("\u0000"),
Expand Down Expand Up @@ -390,6 +437,20 @@ export function SkillsOverview({
onSearchChange={onQueryChange}
controls={
<>
<ResourceMultiSelectMenu
label="Source"
icon="PackageReceive"
selectedValues={sourceFilters}
options={sourceOptions}
selectedLabel={(options) =>
options.map((option) => option.label).join(", ")
}
onChange={(values) =>
setSourceFilters(
values.filter(isResourceSkillSourceFilter),
)
}
/>
<ResourceMultiSelectMenu
label="Provider"
icon="Layers"
Expand Down
103 changes: 88 additions & 15 deletions apps/app/src/views/SkillsView.test.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -270,23 +270,96 @@ describe("SkillsOverview", () => {
);
});

it("labels skills bundled with plugins", () => {
const markup = render({
skills: [
makeSkill({
name: "automations",
provider: null,
scope: "plugin",
pluginId: "automations",
manageable: false,
}),
],
});
it("defaults to BB official on and Included off, then toggles Included independently", async () => {
renderDom(
<SkillsOverview
skills={[
makeSkill({
name: "official-skill",
provider: null,
scope: "bb-builtin",
manageable: false,
}),
makeSkill({
name: "automations",
provider: null,
scope: "plugin",
pluginId: "automations",
manageable: false,
}),
makeSkill({
name: "user-skill",
provider: null,
scope: "bb-user",
}),
]}
isLoading={false}
hasError={false}
onCreateSkill={() => {}}
onSelectSkill={() => {}}
/>,
);

expect(markup).toContain(">Included<");
expect(markup).toContain(
'aria-label="automations is included with Automations (bb plugin)"',
expect(screen.getByText("official-skill")).toBeTruthy();
expect(screen.getByText("user-skill")).toBeTruthy();
expect(screen.queryByText("automations")).toBeNull();
fireEvent.pointerDown(screen.getByRole("button", { name: "BB official" }));
expect(
screen
.getByRole("menuitemcheckbox", { name: "BB official" })
.getAttribute("aria-checked"),
).toBe("true");
expect(
screen
.getByRole("menuitemcheckbox", { name: "Included" })
.getAttribute("aria-checked"),
).toBe("false");
fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "Included" }));

expect(await screen.findByText("automations")).toBeTruthy();
expect(
screen.getByLabelText(
"automations is included with Automations (bb plugin)",
).textContent,
).toBe("Included");
fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "Included" }));
expect(screen.queryByText("automations")).toBeNull();
expect(screen.getByText("official-skill")).toBeTruthy();
});

it("toggles BB official independently from Included", async () => {
renderDom(
<SkillsOverview
skills={[
makeSkill({
name: "official-skill",
provider: null,
scope: "bb-builtin",
manageable: false,
}),
makeSkill({
name: "automations",
provider: null,
scope: "plugin",
pluginId: "automations",
manageable: false,
}),
]}
isLoading={false}
hasError={false}
onCreateSkill={() => {}}
onSelectSkill={() => {}}
/>,
);

fireEvent.pointerDown(screen.getByRole("button", { name: "BB official" }));
fireEvent.click(screen.getByRole("menuitemcheckbox", { name: "Included" }));
fireEvent.click(
screen.getByRole("menuitemcheckbox", { name: "BB official" }),
);

expect(await screen.findByText("automations")).toBeTruthy();
expect(screen.queryByText("official-skill")).toBeNull();
});

it("renders browse content as the active full-page collection mode", () => {
Expand Down
Loading