feat(vscode): add marketplace type filters

This commit is contained in:
Mark IJbema
2026-06-24 08:21:35 +02:00
parent 455848d46b
commit 34540a959f
5 changed files with 90 additions and 16 deletions
@@ -113,9 +113,12 @@ describe("Marketplace installation metadata", () => {
]
const metadata = { project: { "mcp:warehouse": { type: "mcp" } }, global: {} }
expect(filterItems(items, metadata, "reviewer", "all", []).map((item) => item.id)).toEqual(["reviewer"])
expect(filterItems(items, metadata, "", "all", ["business"]).map((item) => item.id)).toEqual(["campaign-writer"])
expect(filterItems(items, metadata, "", "installed", []).map((item) => item.id)).toEqual(["warehouse"])
expect(filterItems(items, metadata, "reviewer", "all", [], []).map((item) => item.id)).toEqual(["reviewer"])
expect(filterItems(items, metadata, "", "all", ["business"], []).map((item) => item.id)).toEqual([
"campaign-writer",
])
expect(filterItems(items, metadata, "", "installed", [], []).map((item) => item.id)).toEqual(["warehouse"])
expect(filterItems(items, metadata, "", "all", [], ["mcp"]).map((item) => item.id)).toEqual(["warehouse"])
})
})
@@ -49,7 +49,7 @@ export const ItemCard = (props: Props) => {
{name()}
</span>
</Show>
<Tag class="marketplace-badge-type">{type()}</Tag>
<Tag class={`marketplace-badge-type marketplace-type-${props.item.type}`}>{type()}</Tag>
</div>
<Show when={props.item.author}>
<span class="marketplace-card-author">
@@ -33,6 +33,7 @@ export const MarketplaceListView = (props: Props) => {
const { t } = useLanguage()
const [search, setSearch] = createSignal("")
const [status, setStatus] = createSignal<StatusOption>({ value: "all", label: t("marketplace.filter.all") })
const [types, setTypes] = createSignal<MarketplaceItem["type"][]>([])
const [categories, setCategories] = createSignal<string[]>([])
const options = (): StatusOption[] => [
@@ -47,8 +48,18 @@ export const MarketplaceListView = (props: Props) => {
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
.join(" ")
const allTypes = createMemo(() => {
const available = new Set(props.items.map((item) => item.type))
return (["agent", "mcp", "skill"] as const).filter((type) => available.has(type))
})
const allCategories = createMemo(() => Array.from(new Set(props.items.map((item) => item.category))).sort())
const typeLabel = (type: MarketplaceItem["type"]) => {
if (type === "mcp") return t("marketplace.badge.mcpServer")
if (type === "agent") return t("marketplace.remove.type.agent")
return t("marketplace.remove.type.skill")
}
createEffect(() => {
const available = new Set(allCategories())
setCategories((current) => {
@@ -57,6 +68,15 @@ export const MarketplaceListView = (props: Props) => {
})
})
const toggleType = (type: MarketplaceItem["type"]) => {
const current = types()
if (current.includes(type)) {
setTypes(current.filter((value) => value !== type))
return
}
setTypes([...current, type])
}
const toggleCategory = (category: string) => {
const current = categories()
if (current.includes(category)) {
@@ -66,7 +86,9 @@ export const MarketplaceListView = (props: Props) => {
setCategories([...current, category])
}
const filtered = createMemo(() => filterItems(props.items, props.metadata, search(), status().value, categories()))
const filtered = createMemo(() =>
filterItems(props.items, props.metadata, search(), status().value, categories(), types()),
)
return (
<div class="marketplace-list">
@@ -82,6 +104,22 @@ export const MarketplaceListView = (props: Props) => {
onSelect={(v: StatusOption | undefined) => v && setStatus(v)}
/>
</div>
<Show when={allTypes().length > 1}>
<div class="marketplace-types">
<For each={allTypes()}>
{(type) => (
<button
class={`marketplace-type-filter marketplace-type-${type}`}
classList={{ active: types().includes(type) }}
aria-pressed={types().includes(type)}
onClick={() => toggleType(type)}
>
<Tag>{typeLabel(type)}</Tag>
</button>
)}
</For>
</div>
</Show>
<Show when={allCategories().length > 0}>
<div class="marketplace-categories">
<For each={allCategories()}>
@@ -27,14 +27,16 @@
flex: 1;
}
/* Category filters */
/* Type and category filters */
.marketplace-types,
.marketplace-categories {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.marketplace-type-filter,
.marketplace-category-filter {
all: unset;
cursor: pointer;
@@ -42,15 +44,18 @@
transition: opacity 0.15s ease;
}
.marketplace-type-filter:hover,
.marketplace-category-filter:hover {
opacity: 0.8;
}
.marketplace-type-filter:focus-visible,
.marketplace-category-filter:focus-visible {
outline: 1px solid var(--vscode-focusBorder);
outline-offset: 2px;
}
.marketplace-type-filter.active,
.marketplace-category-filter.active {
opacity: 1;
}
@@ -200,6 +205,28 @@
.marketplace-badge-type {
flex-shrink: 0;
letter-spacing: 0.4px;
}
.marketplace-type-agent [data-component="tag"],
.marketplace-badge-type.marketplace-type-agent {
color: var(--text-on-success-base) !important;
background-color: color-mix(in srgb, var(--border-success-base) 14%, transparent) !important;
border-color: var(--border-success-base) !important;
}
.marketplace-type-mcp [data-component="tag"],
.marketplace-badge-type.marketplace-type-mcp {
color: var(--text-on-info-base) !important;
background-color: color-mix(in srgb, var(--border-info-base) 14%, transparent) !important;
border-color: var(--border-info-base) !important;
}
.marketplace-type-skill [data-component="tag"],
.marketplace-badge-type.marketplace-type-skill {
color: var(--text-on-warning-base) !important;
background-color: color-mix(in srgb, var(--border-warning-base) 14%, transparent) !important;
border-color: var(--border-warning-base) !important;
}
.marketplace-badge-installed {
@@ -20,30 +20,36 @@ export function installedScopes(
return scopes
}
function matches(item: MarketplaceItem, query: string) {
const skill = item.type === "skill" ? item : undefined
return (
item.id.toLowerCase().includes(query) ||
item.name.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query) ||
item.type.includes(query) ||
(item.author?.toLowerCase().includes(query) ?? false) ||
(skill?.displayName.toLowerCase().includes(query) ?? false)
)
}
export function filterItems(
items: MarketplaceItem[],
metadata: MarketplaceInstalledMetadata,
search: string,
status: string,
categories: string[],
types: MarketplaceItem["type"][],
): MarketplaceItem[] {
const query = search.trim().toLowerCase()
return items
.filter((item) => {
if (status === "installed" && !isInstalled(item.id, item.type, metadata)) return false
if (status === "notInstalled" && isInstalled(item.id, item.type, metadata)) return false
if (types.length > 0 && !types.includes(item.type)) return false
if (categories.length > 0 && !categories.includes(item.category)) return false
if (!query) return true
const skill = item.type === "skill" ? item : undefined
return (
item.id.toLowerCase().includes(query) ||
item.name.toLowerCase().includes(query) ||
item.description.toLowerCase().includes(query) ||
item.category.toLowerCase().includes(query) ||
item.type.includes(query) ||
(item.author?.toLowerCase().includes(query) ?? false) ||
(skill?.displayName.toLowerCase().includes(query) ?? false)
)
return matches(item, query)
})
.sort((a, b) => a.name.localeCompare(b.name))
}