mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-28 19:11:03 +08:00
feat(vscode): add marketplace type filters
This commit is contained in:
@@ -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))
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user