mirror of
https://github.com/Kilo-Org/kilocode.git
synced 2026-08-29 03:44:06 +08:00
feat(vscode): unify marketplace browsing
This commit is contained in:
@@ -0,0 +1,5 @@
|
||||
---
|
||||
"kilo-code": minor
|
||||
---
|
||||
|
||||
Browse skills, agents, and MCP servers together and filter them by category.
|
||||
@@ -5,6 +5,10 @@ import { MarketplacePaths } from "./paths"
|
||||
|
||||
type Entry = [string, { type: string }]
|
||||
|
||||
function entry(id: string, type: "agent" | "mcp" | "skill"): Entry {
|
||||
return [`${type}:${id}`, { type }]
|
||||
}
|
||||
|
||||
export interface CliSkill {
|
||||
name: string
|
||||
location: string
|
||||
@@ -52,7 +56,7 @@ export class InstallationDetector {
|
||||
? !!workspace && this.isProjectSkill(s.location, workspace)
|
||||
: !workspace || !this.isProjectSkill(s.location, workspace),
|
||||
)
|
||||
.map((s) => [s.name, { type: "skill" }])
|
||||
.map((skill) => entry(skill.name, "skill"))
|
||||
}
|
||||
|
||||
/** Scan .kilo/agents/*.md files to detect installed marketplace agents. */
|
||||
@@ -60,7 +64,7 @@ export class InstallationDetector {
|
||||
const dir = this.paths.agentsDir(scope, workspace)
|
||||
try {
|
||||
const files = await fs.readdir(dir)
|
||||
return files.filter((f) => f.endsWith(".md")).map((f) => [path.basename(f, ".md"), { type: "agent" }] as Entry)
|
||||
return files.filter((file) => file.endsWith(".md")).map((file) => entry(path.basename(file, ".md"), "agent"))
|
||||
} catch (err) {
|
||||
if ((err as NodeJS.ErrnoException).code !== "ENOENT") {
|
||||
console.warn(`Failed to detect agent files from ${dir}:`, err)
|
||||
@@ -78,13 +82,13 @@ export class InstallationDetector {
|
||||
|
||||
if (parsed?.mcp && typeof parsed.mcp === "object") {
|
||||
for (const key of Object.keys(parsed.mcp)) {
|
||||
entries.push([key, { type: "mcp" }])
|
||||
entries.push(entry(key, "mcp"))
|
||||
}
|
||||
}
|
||||
|
||||
if (parsed?.agent && typeof parsed.agent === "object") {
|
||||
for (const key of Object.keys(parsed.agent)) {
|
||||
entries.push([key, { type: "agent" }])
|
||||
entries.push(entry(key, "agent"))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface MarketplaceItemBase {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
author?: string
|
||||
authorUrl?: string
|
||||
tags?: string[]
|
||||
prerequisites?: string[]
|
||||
}
|
||||
|
||||
@@ -52,7 +52,6 @@ export interface RawSkill {
|
||||
|
||||
export interface SkillMarketplaceItem extends MarketplaceItemBase {
|
||||
type: "skill"
|
||||
category: string
|
||||
githubUrl: string
|
||||
content: string
|
||||
displayName: string
|
||||
|
||||
@@ -7,6 +7,8 @@ import {
|
||||
type MarketplaceRemoveContext,
|
||||
} from "../../src/services/marketplace/actions"
|
||||
import type { McpMarketplaceItem } from "../../src/services/marketplace/types"
|
||||
import { filterItems, installedScopes } from "../../webview-ui/src/components/marketplace/utils"
|
||||
import type { MarketplaceItem } from "../../webview-ui/src/types/marketplace"
|
||||
|
||||
const project = "/repo"
|
||||
const storage = vscode.Uri.file("/storage")
|
||||
@@ -18,6 +20,7 @@ const item: McpMarketplaceItem = {
|
||||
type: "mcp",
|
||||
name: "Memory",
|
||||
description: "",
|
||||
category: "development",
|
||||
url: "",
|
||||
content: "",
|
||||
}
|
||||
@@ -62,6 +65,60 @@ afterEach(() => {
|
||||
fs.writeFile = original.writeFile
|
||||
})
|
||||
|
||||
describe("Marketplace installation metadata", () => {
|
||||
it("tracks colliding IDs independently by item type", () => {
|
||||
const metadata = {
|
||||
project: {
|
||||
"mcp:dbt": { type: "mcp" },
|
||||
"skill:dbt": { type: "skill" },
|
||||
},
|
||||
global: {},
|
||||
}
|
||||
|
||||
expect(installedScopes("dbt", "mcp", metadata)).toEqual(["project"])
|
||||
expect(installedScopes("dbt", "skill", metadata)).toEqual(["project"])
|
||||
expect(installedScopes("dbt", "agent", metadata)).toEqual([])
|
||||
})
|
||||
|
||||
it("filters the mixed list by search, category, and status", () => {
|
||||
const items: MarketplaceItem[] = [
|
||||
{
|
||||
type: "agent",
|
||||
id: "reviewer",
|
||||
name: "Code Reviewer",
|
||||
description: "Reviews code",
|
||||
category: "development",
|
||||
content: { mode: "all", description: "Reviews code", prompt: "Review" },
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
id: "warehouse",
|
||||
name: "Warehouse",
|
||||
description: "Queries data",
|
||||
category: "data",
|
||||
url: "https://example.com",
|
||||
content: "{}",
|
||||
},
|
||||
{
|
||||
type: "skill",
|
||||
id: "campaign-writer",
|
||||
name: "Campaign Writer",
|
||||
displayName: "Campaign Writer",
|
||||
description: "Writes campaigns",
|
||||
category: "business",
|
||||
displayCategory: "Business",
|
||||
githubUrl: "https://example.com",
|
||||
content: "https://example.com/skill.tar.gz",
|
||||
},
|
||||
]
|
||||
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"])
|
||||
})
|
||||
})
|
||||
|
||||
describe("Marketplace legacy MCP cleanup", () => {
|
||||
it("preserves global legacy config during project removal", async () => {
|
||||
const files = setup()
|
||||
|
||||
@@ -55,6 +55,7 @@ describe("MarketplaceInstaller MCP format normalization", () => {
|
||||
id: "memory",
|
||||
name: "Memory",
|
||||
description: "test",
|
||||
category: "development",
|
||||
url: "https://example.com",
|
||||
content: JSON.stringify({
|
||||
command: "npx",
|
||||
@@ -81,6 +82,7 @@ describe("MarketplaceInstaller MCP format normalization", () => {
|
||||
id: "myremote",
|
||||
name: "Remote",
|
||||
description: "test",
|
||||
category: "development",
|
||||
url: "https://example.com",
|
||||
content: JSON.stringify({
|
||||
type: "sse",
|
||||
@@ -105,6 +107,7 @@ describe("MarketplaceInstaller MCP format normalization", () => {
|
||||
id: "already",
|
||||
name: "Already Done",
|
||||
description: "test",
|
||||
category: "development",
|
||||
url: "https://example.com",
|
||||
content: JSON.stringify({
|
||||
type: "local",
|
||||
@@ -153,6 +156,7 @@ describe("MarketplaceInstaller skills", () => {
|
||||
id: "test-mcp",
|
||||
name: "Test MCP",
|
||||
description: "test",
|
||||
category: "development",
|
||||
url: "https://example.com",
|
||||
content: "{}",
|
||||
},
|
||||
@@ -164,6 +168,7 @@ describe("MarketplaceInstaller skills", () => {
|
||||
id: "test-agent",
|
||||
name: "Test Agent",
|
||||
description: "test",
|
||||
category: "development",
|
||||
content: { mode: "all", description: "test", prompt: "test" },
|
||||
},
|
||||
"project",
|
||||
|
||||
@@ -23,6 +23,11 @@ export const ItemCard = (props: Props) => {
|
||||
const scopes = () => installedScopes(props.item.id, props.item.type, props.metadata)
|
||||
const installed = () => scopes().length > 0
|
||||
const name = () => props.displayName ?? props.item.name
|
||||
const type = () => {
|
||||
if (props.item.type === "mcp") return t("marketplace.badge.mcpServer")
|
||||
if (props.item.type === "agent") return t("marketplace.remove.type.agent")
|
||||
return t("marketplace.remove.type.skill")
|
||||
}
|
||||
const [expanded, setExpanded] = createSignal(false)
|
||||
const [clamped, setClamped] = createSignal(false)
|
||||
let ref: HTMLParagraphElement | undefined
|
||||
@@ -44,6 +49,7 @@ export const ItemCard = (props: Props) => {
|
||||
{name()}
|
||||
</span>
|
||||
</Show>
|
||||
<Tag class="marketplace-badge-type">{type()}</Tag>
|
||||
</div>
|
||||
<Show when={props.item.author}>
|
||||
<span class="marketplace-card-author">
|
||||
|
||||
+35
-54
@@ -1,4 +1,4 @@
|
||||
import { createSignal, createMemo, For, Show } from "solid-js"
|
||||
import { createSignal, createMemo, createEffect, For, Show } from "solid-js"
|
||||
import { TextField } from "@kilocode/kilo-ui/text-field"
|
||||
import { Select } from "@kilocode/kilo-ui/select"
|
||||
import { Tag } from "@kilocode/kilo-ui/tag"
|
||||
@@ -10,7 +10,7 @@ import type {
|
||||
MarketplaceInstalledMetadata,
|
||||
} from "../../types/marketplace"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { isInstalled } from "./utils"
|
||||
import { filterItems } from "./utils"
|
||||
import { ItemCard } from "./ItemCard"
|
||||
import { MarketplaceContribute } from "./MarketplaceContribute"
|
||||
|
||||
@@ -23,7 +23,6 @@ interface Props {
|
||||
items: MarketplaceItem[]
|
||||
metadata: MarketplaceInstalledMetadata
|
||||
fetching: boolean
|
||||
type: "mcp" | "agent" | "skill"
|
||||
searchPlaceholder: string
|
||||
emptyMessage: string
|
||||
onInstall: (item: MarketplaceItem) => void
|
||||
@@ -34,7 +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 [tags, setTags] = createSignal<string[]>([])
|
||||
const [categories, setCategories] = createSignal<string[]>([])
|
||||
|
||||
const options = (): StatusOption[] => [
|
||||
{ value: "all", label: t("marketplace.filter.all") },
|
||||
@@ -42,52 +41,33 @@ export const MarketplaceListView = (props: Props) => {
|
||||
{ value: "notInstalled", label: t("marketplace.filter.notInstalled") },
|
||||
]
|
||||
|
||||
const tagsFor = (item: MarketplaceItem): string[] => {
|
||||
if (item.type === "skill") return [(item as SkillMarketplaceItem).displayCategory]
|
||||
return item.tags ?? []
|
||||
}
|
||||
const label = (category: string) =>
|
||||
category
|
||||
.split("-")
|
||||
.map((word) => word.charAt(0).toUpperCase() + word.slice(1))
|
||||
.join(" ")
|
||||
|
||||
const allTags = createMemo(() => {
|
||||
const counts = new Map<string, number>()
|
||||
for (const item of props.items) {
|
||||
for (const tag of tagsFor(item)) counts.set(tag, (counts.get(tag) ?? 0) + 1)
|
||||
}
|
||||
const min = props.type === "mcp" ? 5 : 1
|
||||
return Array.from(counts.entries())
|
||||
.filter(([, n]) => n >= min)
|
||||
.map(([tag]) => tag)
|
||||
.sort()
|
||||
})
|
||||
const allCategories = createMemo(() => Array.from(new Set(props.items.map((item) => item.category))).sort())
|
||||
|
||||
const toggleTag = (tag: string) => {
|
||||
const current = tags()
|
||||
if (current.includes(tag)) {
|
||||
setTags(current.filter((t) => t !== tag))
|
||||
} else {
|
||||
setTags([...current, tag])
|
||||
}
|
||||
}
|
||||
|
||||
const filtered = createMemo(() => {
|
||||
const q = search().toLowerCase()
|
||||
const s = status().value
|
||||
const active = tags()
|
||||
return props.items.filter((item) => {
|
||||
if (s === "installed" && !isInstalled(item.id, item.type, props.metadata)) return false
|
||||
if (s === "notInstalled" && isInstalled(item.id, item.type, props.metadata)) return false
|
||||
if (active.length > 0 && !active.some((tag) => tagsFor(item).includes(tag))) return false
|
||||
if (!q) return true
|
||||
const skill = item.type === "skill" ? (item as SkillMarketplaceItem) : undefined
|
||||
return (
|
||||
item.id.toLowerCase().includes(q) ||
|
||||
item.name.toLowerCase().includes(q) ||
|
||||
item.description.toLowerCase().includes(q) ||
|
||||
(item.author?.toLowerCase().includes(q) ?? false) ||
|
||||
(skill?.displayName.toLowerCase().includes(q) ?? false)
|
||||
)
|
||||
createEffect(() => {
|
||||
const available = new Set(allCategories())
|
||||
setCategories((current) => {
|
||||
const next = current.filter((category) => available.has(category))
|
||||
return next.length === current.length ? current : next
|
||||
})
|
||||
})
|
||||
|
||||
const toggleCategory = (category: string) => {
|
||||
const current = categories()
|
||||
if (current.includes(category)) {
|
||||
setCategories(current.filter((value) => value !== category))
|
||||
return
|
||||
}
|
||||
setCategories([...current, category])
|
||||
}
|
||||
|
||||
const filtered = createMemo(() => filterItems(props.items, props.metadata, search(), status().value, categories()))
|
||||
|
||||
return (
|
||||
<div class="marketplace-list">
|
||||
<div class="marketplace-filters">
|
||||
@@ -102,16 +82,17 @@ export const MarketplaceListView = (props: Props) => {
|
||||
onSelect={(v: StatusOption | undefined) => v && setStatus(v)}
|
||||
/>
|
||||
</div>
|
||||
<Show when={allTags().length > 0}>
|
||||
<div class="marketplace-active-tags">
|
||||
<For each={allTags()}>
|
||||
{(tag) => (
|
||||
<Show when={allCategories().length > 0}>
|
||||
<div class="marketplace-categories">
|
||||
<For each={allCategories()}>
|
||||
{(category) => (
|
||||
<button
|
||||
class="marketplace-tag-filter"
|
||||
classList={{ active: tags().includes(tag) }}
|
||||
onClick={() => toggleTag(tag)}
|
||||
class="marketplace-category-filter"
|
||||
classList={{ active: categories().includes(category) }}
|
||||
aria-pressed={categories().includes(category)}
|
||||
onClick={() => toggleCategory(category)}
|
||||
>
|
||||
<Tag>{tag}</Tag>
|
||||
<Tag>{label(category)}</Tag>
|
||||
</button>
|
||||
)}
|
||||
</For>
|
||||
@@ -147,7 +128,7 @@ export const MarketplaceListView = (props: Props) => {
|
||||
linkUrl={skill?.githubUrl ?? mcp?.url}
|
||||
onInstall={props.onInstall}
|
||||
onRemove={props.onRemove}
|
||||
footer={<For each={tagsFor(item)}>{(tag) => <Tag>{tag}</Tag>}</For>}
|
||||
footer={<Tag>{label(item.category)}</Tag>}
|
||||
/>
|
||||
)
|
||||
}}
|
||||
|
||||
@@ -1,18 +1,11 @@
|
||||
import { createSignal, createMemo, createEffect, onCleanup, onMount, Show } from "solid-js"
|
||||
import { Tabs } from "@kilocode/kilo-ui/tabs"
|
||||
import { createSignal, createEffect, onCleanup, onMount, Show } from "solid-js"
|
||||
import { Card } from "@kilocode/kilo-ui/card"
|
||||
import { Button } from "@kilocode/kilo-ui/button"
|
||||
import { useVSCode } from "../../context/vscode"
|
||||
import { useServer } from "../../context/server"
|
||||
import { useLanguage } from "../../context/language"
|
||||
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
|
||||
import type {
|
||||
MarketplaceItem,
|
||||
McpMarketplaceItem,
|
||||
AgentMarketplaceItem,
|
||||
SkillMarketplaceItem,
|
||||
MarketplaceInstalledMetadata,
|
||||
} from "../../types/marketplace"
|
||||
import type { MarketplaceItem, MarketplaceInstalledMetadata } from "../../types/marketplace"
|
||||
import { TelemetryEventName } from "../../../../src/services/telemetry/types"
|
||||
import { MarketplaceListView } from "./MarketplaceListView"
|
||||
import { InstallModal } from "./InstallModal"
|
||||
@@ -31,14 +24,9 @@ export const MarketplaceView = () => {
|
||||
const [metadata, setMetadata] = createSignal<MarketplaceInstalledMetadata>(EMPTY_METADATA)
|
||||
const [fetching, setFetching] = createSignal(true)
|
||||
const [errors, setErrors] = createSignal<string[]>([])
|
||||
const [tab, setTab] = createSignal("agent")
|
||||
const [pending, setPending] = createSignal<{ item: MarketplaceItem; scope: "project" | "global" } | null>(null)
|
||||
const [showMigrationBanner, setShowMigrationBanner] = createSignal(false)
|
||||
|
||||
const skills = createMemo(() => items().filter((i): i is SkillMarketplaceItem => i.type === "skill"))
|
||||
const mcps = createMemo(() => items().filter((i): i is McpMarketplaceItem => i.type === "mcp"))
|
||||
const agents = createMemo(() => items().filter((i): i is AgentMarketplaceItem => i.type === "agent"))
|
||||
|
||||
const fetchData = () => {
|
||||
setFetching(true)
|
||||
vscode.postMessage({ type: "fetchMarketplaceData" })
|
||||
@@ -158,62 +146,23 @@ export const MarketplaceView = () => {
|
||||
))}
|
||||
</Show>
|
||||
|
||||
<Tabs value={tab()} onChange={setTab} class="marketplace-tabs-root">
|
||||
<Tabs.List>
|
||||
<Tabs.Trigger value="agent">{t("marketplace.tab.agents")}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="mcp">{t("marketplace.tab.mcp")}</Tabs.Trigger>
|
||||
<Tabs.Trigger value="skill">{t("marketplace.tab.skills")}</Tabs.Trigger>
|
||||
</Tabs.List>
|
||||
|
||||
<div class="marketplace-content">
|
||||
<Tabs.Content value="agent">
|
||||
<Show when={showMigrationBanner()}>
|
||||
<Card variant="info" class="marketplace-error-banner">
|
||||
<span>{t("marketplace.migration.notice")}</span>
|
||||
<Button variant="ghost" size="small" onClick={dismissMigrationBanner}>
|
||||
{t("marketplace.error.dismiss")}
|
||||
</Button>
|
||||
</Card>
|
||||
</Show>
|
||||
<MarketplaceListView
|
||||
items={agents()}
|
||||
metadata={metadata()}
|
||||
fetching={fetching()}
|
||||
type="agent"
|
||||
searchPlaceholder={t("marketplace.search")}
|
||||
emptyMessage={t("marketplace.empty")}
|
||||
onInstall={handleInstall}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="mcp">
|
||||
<MarketplaceListView
|
||||
items={mcps()}
|
||||
metadata={metadata()}
|
||||
fetching={fetching()}
|
||||
type="mcp"
|
||||
searchPlaceholder={t("marketplace.search")}
|
||||
emptyMessage={t("marketplace.empty")}
|
||||
onInstall={handleInstall}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
|
||||
<Tabs.Content value="skill">
|
||||
<MarketplaceListView
|
||||
items={skills()}
|
||||
metadata={metadata()}
|
||||
fetching={fetching()}
|
||||
type="skill"
|
||||
searchPlaceholder={t("marketplace.search")}
|
||||
emptyMessage={t("marketplace.empty")}
|
||||
onInstall={handleInstall}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</Tabs.Content>
|
||||
</div>
|
||||
</Tabs>
|
||||
<Show when={showMigrationBanner()}>
|
||||
<Card variant="info" class="marketplace-error-banner">
|
||||
<span>{t("marketplace.migration.notice")}</span>
|
||||
<Button variant="ghost" size="small" onClick={dismissMigrationBanner}>
|
||||
{t("marketplace.error.dismiss")}
|
||||
</Button>
|
||||
</Card>
|
||||
</Show>
|
||||
<MarketplaceListView
|
||||
items={items()}
|
||||
metadata={metadata()}
|
||||
fetching={fetching()}
|
||||
searchPlaceholder={t("marketplace.search")}
|
||||
emptyMessage={t("marketplace.empty")}
|
||||
onInstall={handleInstall}
|
||||
onRemove={handleRemove}
|
||||
/>
|
||||
</div>
|
||||
)
|
||||
}
|
||||
|
||||
@@ -9,25 +9,12 @@
|
||||
gap: 8px;
|
||||
}
|
||||
|
||||
.marketplace-tabs-root {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
flex: 1;
|
||||
min-height: 0;
|
||||
}
|
||||
|
||||
.marketplace-content {
|
||||
flex: 1;
|
||||
overflow: auto;
|
||||
}
|
||||
|
||||
/* List layout */
|
||||
|
||||
.marketplace-list {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: 12px;
|
||||
padding-top: 12px;
|
||||
}
|
||||
|
||||
.marketplace-filters {
|
||||
@@ -40,30 +27,31 @@
|
||||
flex: 1;
|
||||
}
|
||||
|
||||
.marketplace-category-filters {
|
||||
overflow-x: auto;
|
||||
}
|
||||
/* Category filters */
|
||||
|
||||
/* Tag filters */
|
||||
|
||||
.marketplace-active-tags {
|
||||
.marketplace-categories {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: 4px;
|
||||
}
|
||||
|
||||
.marketplace-tag-filter {
|
||||
.marketplace-category-filter {
|
||||
all: unset;
|
||||
cursor: pointer;
|
||||
opacity: 0.5;
|
||||
transition: opacity 0.15s ease;
|
||||
}
|
||||
|
||||
.marketplace-tag-filter:hover {
|
||||
.marketplace-category-filter:hover {
|
||||
opacity: 0.8;
|
||||
}
|
||||
|
||||
.marketplace-tag-filter.active {
|
||||
.marketplace-category-filter:focus-visible {
|
||||
outline: 1px solid var(--vscode-focusBorder);
|
||||
outline-offset: 2px;
|
||||
}
|
||||
|
||||
.marketplace-category-filter.active {
|
||||
opacity: 1;
|
||||
}
|
||||
|
||||
@@ -210,6 +198,10 @@
|
||||
|
||||
/* Installed badge */
|
||||
|
||||
.marketplace-badge-type {
|
||||
flex-shrink: 0;
|
||||
}
|
||||
|
||||
.marketplace-badge-installed {
|
||||
color: var(--text-on-success-base) !important;
|
||||
border-color: var(--border-success-base) !important;
|
||||
@@ -326,11 +318,11 @@ body.vscode-high-contrast-light {
|
||||
border: 1px solid var(--vscode-contrastBorder, transparent);
|
||||
}
|
||||
|
||||
.marketplace-tag-filter {
|
||||
.marketplace-category-filter {
|
||||
border: 1px solid var(--vscode-contrastBorder, transparent);
|
||||
}
|
||||
|
||||
.marketplace-tag-filter.active {
|
||||
.marketplace-category-filter.active {
|
||||
border-color: var(--vscode-contrastActiveBorder, var(--vscode-focusBorder));
|
||||
}
|
||||
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
import type { MarketplaceInstalledMetadata } from "../../types/marketplace"
|
||||
import type { MarketplaceInstalledMetadata, MarketplaceItem } from "../../types/marketplace"
|
||||
|
||||
export function isInstalled(
|
||||
id: string,
|
||||
@@ -14,7 +14,36 @@ export function installedScopes(
|
||||
metadata: MarketplaceInstalledMetadata,
|
||||
): ("project" | "global")[] {
|
||||
const scopes: ("project" | "global")[] = []
|
||||
if (metadata.project[id]?.type === type) scopes.push("project")
|
||||
if (metadata.global[id]?.type === type) scopes.push("global")
|
||||
const key = `${type}:${id}`
|
||||
if (metadata.project[key]?.type === type) scopes.push("project")
|
||||
if (metadata.global[key]?.type === type) scopes.push("global")
|
||||
return scopes
|
||||
}
|
||||
|
||||
export function filterItems(
|
||||
items: MarketplaceItem[],
|
||||
metadata: MarketplaceInstalledMetadata,
|
||||
search: string,
|
||||
status: string,
|
||||
categories: string[],
|
||||
): 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 (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)
|
||||
)
|
||||
})
|
||||
.sort((a, b) => a.name.localeCompare(b.name))
|
||||
}
|
||||
|
||||
@@ -115,7 +115,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [
|
||||
'{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-github"], "env": { "GITHUB_TOKEN": "${GITHUB_TOKEN}" } }',
|
||||
parameters: [{ name: "GitHub Token", key: "GITHUB_TOKEN", placeholder: "ghp_xxxxxxxxxxxx" }],
|
||||
author: "Anthropic",
|
||||
tags: ["version-control", "development"],
|
||||
category: "development",
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
@@ -143,7 +143,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [
|
||||
},
|
||||
],
|
||||
author: "Anthropic",
|
||||
tags: ["database", "sql"],
|
||||
category: "data",
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
@@ -153,7 +153,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [
|
||||
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/filesystem",
|
||||
content: '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-filesystem", "${ALLOWED_DIR}"] }',
|
||||
parameters: [{ name: "Allowed Directory", key: "ALLOWED_DIR", placeholder: "/path/to/directory" }],
|
||||
tags: ["filesystem", "development"],
|
||||
category: "development",
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
@@ -165,7 +165,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [
|
||||
'{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-slack"], "env": { "SLACK_TOKEN": "${SLACK_TOKEN}" } }',
|
||||
parameters: [{ name: "Slack Bot Token", key: "SLACK_TOKEN", placeholder: "xoxb-xxxxxxxxxxxx" }],
|
||||
author: "Anthropic",
|
||||
tags: ["communication", "productivity"],
|
||||
category: "productivity",
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
@@ -176,7 +176,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [
|
||||
content:
|
||||
'{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-brave-search"], "env": { "BRAVE_API_KEY": "${BRAVE_API_KEY}" } }',
|
||||
parameters: [{ name: "API Key", key: "BRAVE_API_KEY", placeholder: "BSA-xxxxxxxxxxxx" }],
|
||||
tags: ["search", "web"],
|
||||
category: "search",
|
||||
},
|
||||
{
|
||||
type: "mcp",
|
||||
@@ -186,7 +186,7 @@ const MOCK_MCPS: McpMarketplaceItem[] = [
|
||||
url: "https://github.com/modelcontextprotocol/servers/tree/main/src/puppeteer",
|
||||
content: '{ "command": "npx", "args": ["-y", "@modelcontextprotocol/server-puppeteer"] }',
|
||||
prerequisites: ["Chrome or Chromium must be installed"],
|
||||
tags: ["browser", "automation", "web"],
|
||||
category: "web-automation",
|
||||
},
|
||||
]
|
||||
|
||||
@@ -205,7 +205,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [
|
||||
permission: { read: "allow", edit: "deny", bash: "deny", mcp: "deny", question: "allow" },
|
||||
},
|
||||
author: "Kilo",
|
||||
tags: ["planning", "design"],
|
||||
category: "development",
|
||||
},
|
||||
{
|
||||
type: "agent",
|
||||
@@ -221,7 +221,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [
|
||||
permission: { read: "allow", edit: "deny", bash: "allow", mcp: "deny", question: "allow" },
|
||||
},
|
||||
author: "Kilo",
|
||||
tags: ["review", "quality"],
|
||||
category: "development",
|
||||
},
|
||||
{
|
||||
type: "agent",
|
||||
@@ -235,7 +235,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [
|
||||
options: { displayName: "Documentation Writer" },
|
||||
permission: { read: "allow", edit: "allow", bash: "allow", mcp: "deny", question: "allow" },
|
||||
},
|
||||
tags: ["documentation", "writing"],
|
||||
category: "business",
|
||||
},
|
||||
{
|
||||
type: "agent",
|
||||
@@ -251,7 +251,7 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [
|
||||
permission: { read: "allow", edit: "allow", bash: "allow", mcp: "allow", question: "allow" },
|
||||
},
|
||||
author: "Community",
|
||||
tags: ["testing", "methodology"],
|
||||
category: "development",
|
||||
},
|
||||
{
|
||||
type: "agent",
|
||||
@@ -265,25 +265,33 @@ const MOCK_AGENTS: AgentMarketplaceItem[] = [
|
||||
options: { displayName: "Debugger" },
|
||||
permission: { read: "allow", edit: "allow", bash: "allow", mcp: "deny", question: "allow" },
|
||||
},
|
||||
tags: ["debugging", "troubleshooting"],
|
||||
category: "development",
|
||||
},
|
||||
]
|
||||
|
||||
const EMPTY_METADATA: MarketplaceInstalledMetadata = { project: {}, global: {} }
|
||||
|
||||
const PARTIAL_INSTALLED_SKILLS: MarketplaceInstalledMetadata = {
|
||||
project: { "nextjs-developer": { type: "skill" } },
|
||||
global: { "python-data-science": { type: "skill" } },
|
||||
project: { "skill:nextjs-developer": { type: "skill" } },
|
||||
global: { "skill:python-data-science": { type: "skill" } },
|
||||
}
|
||||
|
||||
const PARTIAL_INSTALLED_MCPS: MarketplaceInstalledMetadata = {
|
||||
project: { "github-mcp": { type: "mcp" } },
|
||||
global: { "postgres-mcp": { type: "mcp" } },
|
||||
project: { "mcp:github-mcp": { type: "mcp" } },
|
||||
global: { "mcp:postgres-mcp": { type: "mcp" } },
|
||||
}
|
||||
|
||||
const PARTIAL_INSTALLED_AGENTS: MarketplaceInstalledMetadata = {
|
||||
project: { architect: { type: "agent" } },
|
||||
global: { reviewer: { type: "agent" } },
|
||||
project: { "agent:architect": { type: "agent" } },
|
||||
global: { "agent:reviewer": { type: "agent" } },
|
||||
}
|
||||
|
||||
const PARTIAL_INSTALLED_MIXED: MarketplaceInstalledMetadata = {
|
||||
project: {
|
||||
"agent:architect": { type: "agent" },
|
||||
"mcp:github-mcp": { type: "mcp" },
|
||||
},
|
||||
global: { "skill:python-data-science": { type: "skill" } },
|
||||
}
|
||||
|
||||
const noop = () => {}
|
||||
@@ -292,8 +300,27 @@ const noop = () => {}
|
||||
// Stories
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const MixedListWithItems: Story = {
|
||||
name: "Mixed list — categories and installed items",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ "max-height": "700px", overflow: "auto", padding: "12px" }}>
|
||||
<MarketplaceListView
|
||||
items={[...MOCK_AGENTS, ...MOCK_MCPS, ...MOCK_SKILLS]}
|
||||
metadata={PARTIAL_INSTALLED_MIXED}
|
||||
fetching={false}
|
||||
searchPlaceholder="Search marketplace..."
|
||||
emptyMessage="No items found"
|
||||
onInstall={noop}
|
||||
onRemove={noop}
|
||||
/>
|
||||
</div>
|
||||
</StoryProviders>
|
||||
),
|
||||
}
|
||||
|
||||
export const SkillsTabWithItems: Story = {
|
||||
name: "Skills tab — with items",
|
||||
name: "Skills list — with items",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -301,7 +328,6 @@ export const SkillsTabWithItems: Story = {
|
||||
items={MOCK_SKILLS}
|
||||
metadata={EMPTY_METADATA}
|
||||
fetching={false}
|
||||
type="skill"
|
||||
searchPlaceholder="Search skills..."
|
||||
emptyMessage="No skills found"
|
||||
onInstall={noop}
|
||||
@@ -313,7 +339,7 @@ export const SkillsTabWithItems: Story = {
|
||||
}
|
||||
|
||||
export const SkillsTabWithInstalled: Story = {
|
||||
name: "Skills tab — some installed",
|
||||
name: "Skills list — some installed",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -321,7 +347,6 @@ export const SkillsTabWithInstalled: Story = {
|
||||
items={MOCK_SKILLS}
|
||||
metadata={PARTIAL_INSTALLED_SKILLS}
|
||||
fetching={false}
|
||||
type="skill"
|
||||
searchPlaceholder="Search skills..."
|
||||
emptyMessage="No skills found"
|
||||
onInstall={noop}
|
||||
@@ -333,7 +358,7 @@ export const SkillsTabWithInstalled: Story = {
|
||||
}
|
||||
|
||||
export const SkillsTabEmpty: Story = {
|
||||
name: "Skills tab — empty state",
|
||||
name: "Skills list — empty state",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "400px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -341,7 +366,6 @@ export const SkillsTabEmpty: Story = {
|
||||
items={[]}
|
||||
metadata={EMPTY_METADATA}
|
||||
fetching={false}
|
||||
type="skill"
|
||||
searchPlaceholder="Search skills..."
|
||||
emptyMessage="No skills found"
|
||||
onInstall={noop}
|
||||
@@ -393,7 +417,7 @@ export const InstalledSkillCard: Story = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const McpTabWithItems: Story = {
|
||||
name: "MCP tab — with items",
|
||||
name: "MCP list — with items",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -401,7 +425,6 @@ export const McpTabWithItems: Story = {
|
||||
items={MOCK_MCPS}
|
||||
metadata={EMPTY_METADATA}
|
||||
fetching={false}
|
||||
type="mcp"
|
||||
searchPlaceholder="Search MCP servers..."
|
||||
emptyMessage="No MCP servers found"
|
||||
onInstall={noop}
|
||||
@@ -413,7 +436,7 @@ export const McpTabWithItems: Story = {
|
||||
}
|
||||
|
||||
export const McpTabWithInstalled: Story = {
|
||||
name: "MCP tab — some installed",
|
||||
name: "MCP list — some installed",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -421,7 +444,6 @@ export const McpTabWithInstalled: Story = {
|
||||
items={MOCK_MCPS}
|
||||
metadata={PARTIAL_INSTALLED_MCPS}
|
||||
fetching={false}
|
||||
type="mcp"
|
||||
searchPlaceholder="Search MCP servers..."
|
||||
emptyMessage="No MCP servers found"
|
||||
onInstall={noop}
|
||||
@@ -433,7 +455,7 @@ export const McpTabWithInstalled: Story = {
|
||||
}
|
||||
|
||||
export const McpTabEmpty: Story = {
|
||||
name: "MCP tab — empty state",
|
||||
name: "MCP list — empty state",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "400px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -441,7 +463,6 @@ export const McpTabEmpty: Story = {
|
||||
items={[]}
|
||||
metadata={EMPTY_METADATA}
|
||||
fetching={false}
|
||||
type="mcp"
|
||||
searchPlaceholder="Search MCP servers..."
|
||||
emptyMessage="No MCP servers found"
|
||||
onInstall={noop}
|
||||
@@ -491,7 +512,7 @@ export const InstalledMcpCard: Story = {
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
export const AgentsTabWithItems: Story = {
|
||||
name: "Agents tab — with items",
|
||||
name: "Agents list — with items",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -499,7 +520,6 @@ export const AgentsTabWithItems: Story = {
|
||||
items={MOCK_AGENTS}
|
||||
metadata={EMPTY_METADATA}
|
||||
fetching={false}
|
||||
type="agent"
|
||||
searchPlaceholder="Search agents..."
|
||||
emptyMessage="No agents found"
|
||||
onInstall={noop}
|
||||
@@ -511,7 +531,7 @@ export const AgentsTabWithItems: Story = {
|
||||
}
|
||||
|
||||
export const AgentsTabWithInstalled: Story = {
|
||||
name: "Agents tab — some installed",
|
||||
name: "Agents list — some installed",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "700px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -519,7 +539,6 @@ export const AgentsTabWithInstalled: Story = {
|
||||
items={MOCK_AGENTS}
|
||||
metadata={PARTIAL_INSTALLED_AGENTS}
|
||||
fetching={false}
|
||||
type="agent"
|
||||
searchPlaceholder="Search agents..."
|
||||
emptyMessage="No agents found"
|
||||
onInstall={noop}
|
||||
@@ -531,7 +550,7 @@ export const AgentsTabWithInstalled: Story = {
|
||||
}
|
||||
|
||||
export const AgentsTabEmpty: Story = {
|
||||
name: "Agents tab — empty state",
|
||||
name: "Agents list — empty state",
|
||||
render: () => (
|
||||
<StoryProviders>
|
||||
<div style={{ width: "420px", height: "400px", overflow: "auto", padding: "12px" }}>
|
||||
@@ -539,7 +558,6 @@ export const AgentsTabEmpty: Story = {
|
||||
items={[]}
|
||||
metadata={EMPTY_METADATA}
|
||||
fetching={false}
|
||||
type="agent"
|
||||
searchPlaceholder="Search agents..."
|
||||
emptyMessage="No agents found"
|
||||
onInstall={noop}
|
||||
|
||||
@@ -16,9 +16,9 @@ export interface MarketplaceItemBase {
|
||||
id: string
|
||||
name: string
|
||||
description: string
|
||||
category: string
|
||||
author?: string
|
||||
authorUrl?: string
|
||||
tags?: string[]
|
||||
prerequisites?: string[]
|
||||
}
|
||||
|
||||
@@ -44,7 +44,6 @@ export interface AgentMarketplaceItem extends MarketplaceItemBase {
|
||||
|
||||
export interface SkillMarketplaceItem extends MarketplaceItemBase {
|
||||
type: "skill"
|
||||
category: string
|
||||
githubUrl: string
|
||||
content: string
|
||||
displayName: string
|
||||
@@ -66,5 +65,5 @@ export interface MarketplaceInstalledMetadata {
|
||||
export interface MarketplaceFilters {
|
||||
type?: string
|
||||
search?: string
|
||||
tags?: string[]
|
||||
categories?: string[]
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user