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
5 changes: 5 additions & 0 deletions .changeset/mix-marketplace-categories.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"kilo-code": minor
---

Browse skills, agents, and MCP servers together and filter them by category.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

This file was deleted.

This file was deleted.

This file was deleted.

Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

This file was deleted.

This file was deleted.

This file was deleted.

12 changes: 8 additions & 4 deletions packages/kilo-vscode/src/services/marketplace/detection.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand Down Expand Up @@ -52,15 +56,15 @@ 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. */
private async detectAgentFiles(scope: "project" | "global", workspace?: string): Promise<Entry[]> {
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)
Expand All @@ -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"))
}
}

Expand Down
3 changes: 1 addition & 2 deletions packages/kilo-vscode/src/services/marketplace/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,9 @@ export interface MarketplaceItemBase {
id: string
name: string
description: string
category: string
author?: string
authorUrl?: string
tags?: string[]
prerequisites?: string[]
}

Expand Down Expand Up @@ -52,7 +52,6 @@ export interface RawSkill {

export interface SkillMarketplaceItem extends MarketplaceItemBase {
type: "skill"
category: string
githubUrl: string
content: string
displayName: string
Expand Down
3 changes: 1 addition & 2 deletions packages/kilo-vscode/tests/accessibility.spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,7 @@ const STORIES = [
{ id: "profile--logged-in-personal", name: "Profile / personal account" },
{ id: "profile--logged-in", name: "Profile / organization account" },
{ id: "settings--providers-configure", name: "Settings / providers empty state" },
{ id: "marketplace--skills-tab-empty", name: "Marketplace / skills empty state" },
{ id: "marketplace--agents-tab-empty", name: "Marketplace / agents empty state" },
{ id: "marketplace--empty-list", name: "Marketplace / empty state" },
{ id: "agentmanager--sidebar-search-open", name: "Agent Manager / sidebar search" },
]

Expand Down
68 changes: 68 additions & 0 deletions packages/kilo-vscode/tests/unit/marketplace-actions.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,8 @@ import {
type MarketplaceRemoveContext,
} from "../../src/services/marketplace/actions"
import type { McpMarketplaceItem } from "../../src/services/marketplace/types"
import { filterItems, installedScopes, retain } 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")
Expand All @@ -18,6 +20,7 @@ const item: McpMarketplaceItem = {
type: "mcp",
name: "Memory",
description: "",
category: "development",
url: "",
content: "",
}
Expand Down Expand Up @@ -62,6 +65,71 @@ 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("removes filters that are no longer available", () => {
expect(retain(["agent", "mcp"], ["mcp", "skill"])).toEqual(["mcp"])
})

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: "web-automation",
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, "web automation", "all", [], []).map((item) => item.id)).toEqual(["warehouse"])
expect(
filterItems(items, metadata, "servidor mcp", "all", [], [], { mcp: "Servidor MCP" }).map((item) => item.id),
).toEqual(["warehouse"])
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"])
})
})

describe("Marketplace legacy MCP cleanup", () => {
it("preserves global legacy config during project removal", async () => {
const files = setup()
Expand Down
5 changes: 5 additions & 0 deletions packages/kilo-vscode/tests/unit/marketplace-installer.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand All @@ -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",
Expand All @@ -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",
Expand Down Expand Up @@ -153,6 +156,7 @@ describe("MarketplaceInstaller skills", () => {
id: "test-mcp",
name: "Test MCP",
description: "test",
category: "development",
url: "https://example.com",
content: "{}",
},
Expand All @@ -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",
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -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
Expand All @@ -44,6 +49,7 @@ export const ItemCard = (props: Props) => {
{name()}
</span>
</Show>
<Tag class={`marketplace-badge-type marketplace-type-${props.item.type}`}>{type()}</Tag>
</div>
<Show when={props.item.author}>
<span class="marketplace-card-author">
Expand Down
Loading
Loading