feat(vscode): unify marketplace browsing

This commit is contained in:
Mark IJbema
2026-06-23 13:42:24 +02:00
parent 8a55ffa283
commit 45eadab047
12 changed files with 239 additions and 195 deletions
+5
View File
@@ -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">
@@ -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[]
}