feat(marketplace): add MCP servers and modes tabs

Implements the full marketplace with all three tabs functional:
- MCP Servers: browse, search, filter by status/tags, install with
  scope selection, method picker, and parameter form
- Modes: browse, search, filter by status/tags, install/remove
- Skills: unchanged from previous PR

Rewrites marketplace components to use kilo-ui Dialog/Card components,
useDialog() for modal management, and vscode.onMessage for result
subscriptions. Adds MarketplaceListView shared component for MCP and
Modes tabs. Full installer with MCP config writing (local/remote),
mode YAML parsing, and template parameter substitution.

Adds 18 new i18n keys per locale (16 languages) for MCP/mode specific
strings (badges, filters, install method, parameters, type labels).
This commit is contained in:
Mark IJbema
2026-03-17 14:27:46 +01:00
parent 01e931cad3
commit 0c92a892ae
28 changed files with 1067 additions and 307 deletions
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"هل أنت متأكد أنك تريد إزالة هذا الـ {{type}}؟ سيؤدي هذا إلى إزالته من تكوين {{scope}} الخاص بك.",
"marketplace.remove.cancel": "إلغاء",
"marketplace.remove.confirm.button": "إزالة",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "بحث...",
"marketplace.filter.all": "جميع العناصر",
"marketplace.filter.notInstalled": "غير مثبت",
"marketplace.empty": "لم يتم العثور على عناصر",
"marketplace.badge.mcpServer": "خادم MCP",
"marketplace.badge.mode": "وضع",
"marketplace.card.by": "بواسطة {{author}}",
"marketplace.install.method": "طريقة التثبيت",
"marketplace.install.parameters": "المعلمات",
"marketplace.install.optional": "(اختياري)",
"marketplace.install.required": "{{name}} مطلوب",
"marketplace.scope.project": "مشروع",
"marketplace.scope.global": "عام",
"marketplace.remove.type.mcp": "خادم MCP",
"marketplace.remove.type.skill": "مهارة",
"marketplace.remove.type.mode": "وضع",
"marketplace.remove.failed": "فشلت إزالة {{name}}",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"Tem certeza que deseja remover este {{type}}? Isso o removerá da sua configuração {{scope}}.",
"marketplace.remove.cancel": "Cancelar",
"marketplace.remove.confirm.button": "Remover",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Pesquisar...",
"marketplace.filter.all": "Todos os Itens",
"marketplace.filter.notInstalled": "Não Instalado",
"marketplace.empty": "Nenhum item encontrado",
"marketplace.badge.mcpServer": "Servidor MCP",
"marketplace.badge.mode": "Modo",
"marketplace.card.by": "por {{author}}",
"marketplace.install.method": "Método de Instalação",
"marketplace.install.parameters": "Parâmetros",
"marketplace.install.optional": "(opcional)",
"marketplace.install.required": "{{name}} é obrigatório",
"marketplace.scope.project": "projeto",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "servidor MCP",
"marketplace.remove.type.skill": "habilidade",
"marketplace.remove.type.mode": "modo",
"marketplace.remove.failed": "Falha ao remover {{name}}",
}
+18
View File
@@ -48,4 +48,22 @@ export const dict = {
"Jeste li sigurni da želite ukloniti ovaj {{type}}? Ovo će ga ukloniti iz vaše {{scope}} konfiguracije.",
"marketplace.remove.cancel": "Otkaži",
"marketplace.remove.confirm.button": "Ukloni",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Pretraži...",
"marketplace.filter.all": "Sve stavke",
"marketplace.filter.notInstalled": "Nije instalirano",
"marketplace.empty": "Nema pronađenih stavki",
"marketplace.badge.mcpServer": "MCP Server",
"marketplace.badge.mode": "Režim",
"marketplace.card.by": "od {{author}}",
"marketplace.install.method": "Metoda instalacije",
"marketplace.install.parameters": "Parametri",
"marketplace.install.optional": "(opciono)",
"marketplace.install.required": "{{name}} je obavezno",
"marketplace.scope.project": "projekat",
"marketplace.scope.global": "globalno",
"marketplace.remove.type.mcp": "MCP server",
"marketplace.remove.type.skill": "vještina",
"marketplace.remove.type.mode": "režim",
"marketplace.remove.failed": "Uklanjanje {{name}} nije uspjelo",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"Er du sikker på, at du vil fjerne denne {{type}}? Dette vil fjerne den fra din {{scope}} konfiguration.",
"marketplace.remove.cancel": "Annuller",
"marketplace.remove.confirm.button": "Fjern",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Søg...",
"marketplace.filter.all": "Alle elementer",
"marketplace.filter.notInstalled": "Ikke installeret",
"marketplace.empty": "Ingen elementer fundet",
"marketplace.badge.mcpServer": "MCP-server",
"marketplace.badge.mode": "Tilstand",
"marketplace.card.by": "af {{author}}",
"marketplace.install.method": "Installationsmetode",
"marketplace.install.parameters": "Parametre",
"marketplace.install.optional": "(valgfrit)",
"marketplace.install.required": "{{name}} er påkrævet",
"marketplace.scope.project": "projekt",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "MCP-server",
"marketplace.remove.type.skill": "færdighed",
"marketplace.remove.type.mode": "tilstand",
"marketplace.remove.failed": "Kunne ikke fjerne {{name}}",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"Sind Sie sicher, dass Sie diese {{type}} entfernen möchten? Dadurch wird sie aus Ihrer {{scope}}-Konfiguration entfernt.",
"marketplace.remove.cancel": "Abbrechen",
"marketplace.remove.confirm.button": "Entfernen",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Suchen...",
"marketplace.filter.all": "Alle Elemente",
"marketplace.filter.notInstalled": "Nicht installiert",
"marketplace.empty": "Keine Elemente gefunden",
"marketplace.badge.mcpServer": "MCP-Server",
"marketplace.badge.mode": "Modus",
"marketplace.card.by": "von {{author}}",
"marketplace.install.method": "Installationsmethode",
"marketplace.install.parameters": "Parameter",
"marketplace.install.optional": "(optional)",
"marketplace.install.required": "{{name}} ist erforderlich",
"marketplace.scope.project": "Projekt",
"marketplace.scope.global": "Global",
"marketplace.remove.type.mcp": "MCP-Server",
"marketplace.remove.type.skill": "Skill",
"marketplace.remove.type.mode": "Modus",
"marketplace.remove.failed": "Fehler beim Entfernen von {{name}}",
}
+18
View File
@@ -45,4 +45,22 @@ export const dict = {
"Are you sure you want to remove this {{type}}? This will remove it from your {{scope}} configuration.",
"marketplace.remove.cancel": "Cancel",
"marketplace.remove.confirm.button": "Remove",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Search...",
"marketplace.filter.all": "All Items",
"marketplace.filter.notInstalled": "Not Installed",
"marketplace.empty": "No items found",
"marketplace.badge.mcpServer": "MCP Server",
"marketplace.badge.mode": "Mode",
"marketplace.card.by": "by {{author}}",
"marketplace.install.method": "Installation Method",
"marketplace.install.parameters": "Parameters",
"marketplace.install.optional": "(optional)",
"marketplace.install.required": "{{name}} is required",
"marketplace.scope.project": "project",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "MCP server",
"marketplace.remove.type.skill": "skill",
"marketplace.remove.type.mode": "mode",
"marketplace.remove.failed": "Failed to remove {{name}}",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"¿Estás seguro de que deseas eliminar este {{type}}? Esto lo eliminará de tu configuración {{scope}}.",
"marketplace.remove.cancel": "Cancelar",
"marketplace.remove.confirm.button": "Eliminar",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Buscar...",
"marketplace.filter.all": "Todos los elementos",
"marketplace.filter.notInstalled": "No instalado",
"marketplace.empty": "No se encontraron elementos",
"marketplace.badge.mcpServer": "Servidor MCP",
"marketplace.badge.mode": "Modo",
"marketplace.card.by": "por {{author}}",
"marketplace.install.method": "Método de instalación",
"marketplace.install.parameters": "Parámetros",
"marketplace.install.optional": "(opcional)",
"marketplace.install.required": "{{name}} es obligatorio",
"marketplace.scope.project": "proyecto",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "servidor MCP",
"marketplace.remove.type.skill": "habilidad",
"marketplace.remove.type.mode": "modo",
"marketplace.remove.failed": "Error al eliminar {{name}}",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"Êtes-vous sûr de vouloir supprimer ce {{type}} ? Cela le supprimera de votre configuration {{scope}}.",
"marketplace.remove.cancel": "Annuler",
"marketplace.remove.confirm.button": "Supprimer",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Rechercher...",
"marketplace.filter.all": "Tous les éléments",
"marketplace.filter.notInstalled": "Non installé",
"marketplace.empty": "Aucun élément trouvé",
"marketplace.badge.mcpServer": "Serveur MCP",
"marketplace.badge.mode": "Mode",
"marketplace.card.by": "par {{author}}",
"marketplace.install.method": "Méthode d'installation",
"marketplace.install.parameters": "Paramètres",
"marketplace.install.optional": "(facultatif)",
"marketplace.install.required": "{{name}} est requis",
"marketplace.scope.project": "projet",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "serveur MCP",
"marketplace.remove.type.skill": "compétence",
"marketplace.remove.type.mode": "mode",
"marketplace.remove.failed": "Échec de la suppression de {{name}}",
}
+18
View File
@@ -41,4 +41,22 @@ export const dict = {
"marketplace.remove.confirm": "この{{type}}を削除してもよろしいですか? これにより、{{scope}}設定から削除されます。",
"marketplace.remove.cancel": "キャンセル",
"marketplace.remove.confirm.button": "削除",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "検索...",
"marketplace.filter.all": "すべてのアイテム",
"marketplace.filter.notInstalled": "未インストール",
"marketplace.empty": "アイテムが見つかりません",
"marketplace.badge.mcpServer": "MCPサーバー",
"marketplace.badge.mode": "モード",
"marketplace.card.by": "作成者: {{author}}",
"marketplace.install.method": "インストール方法",
"marketplace.install.parameters": "パラメーター",
"marketplace.install.optional": "(任意)",
"marketplace.install.required": "{{name}} は必須です",
"marketplace.scope.project": "プロジェクト",
"marketplace.scope.global": "グローバル",
"marketplace.remove.type.mcp": "MCPサーバー",
"marketplace.remove.type.skill": "スキル",
"marketplace.remove.type.mode": "モード",
"marketplace.remove.failed": "{{name}} の削除に失敗しました",
}
+18
View File
@@ -41,4 +41,22 @@ export const dict = {
"marketplace.remove.confirm": "이 {{type}}을(를) 제거하시겠습니까? {{scope}} 구성에서 제거됩니다.",
"marketplace.remove.cancel": "취소",
"marketplace.remove.confirm.button": "제거",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "검색...",
"marketplace.filter.all": "모든 항목",
"marketplace.filter.notInstalled": "설치되지 않음",
"marketplace.empty": "항목을 찾을 수 없음",
"marketplace.badge.mcpServer": "MCP 서버",
"marketplace.badge.mode": "모드",
"marketplace.card.by": "제작: {{author}}",
"marketplace.install.method": "설치 방법",
"marketplace.install.parameters": "매개변수",
"marketplace.install.optional": "(선택 사항)",
"marketplace.install.required": "{{name}}이(가) 필요합니다",
"marketplace.scope.project": "프로젝트",
"marketplace.scope.global": "글로벌",
"marketplace.remove.type.mcp": "MCP 서버",
"marketplace.remove.type.skill": "스킬",
"marketplace.remove.type.mode": "모드",
"marketplace.remove.failed": "{{name}} 제거 실패",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"Er du sikker på at du vil fjerne denne {{type}}? Dette vil fjerne den fra din {{scope}}-konfigurasjon.",
"marketplace.remove.cancel": "Avbryt",
"marketplace.remove.confirm.button": "Fjern",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Søk...",
"marketplace.filter.all": "Alle elementer",
"marketplace.filter.notInstalled": "Ikke installert",
"marketplace.empty": "Ingen elementer funnet",
"marketplace.badge.mcpServer": "MCP-server",
"marketplace.badge.mode": "Modus",
"marketplace.card.by": "av {{author}}",
"marketplace.install.method": "Installasjonsmetode",
"marketplace.install.parameters": "Parametere",
"marketplace.install.optional": "(valgfritt)",
"marketplace.install.required": "{{name}} er påkrevd",
"marketplace.scope.project": "prosjekt",
"marketplace.scope.global": "global",
"marketplace.remove.type.mcp": "MCP-server",
"marketplace.remove.type.skill": "ferdighet",
"marketplace.remove.type.mode": "modus",
"marketplace.remove.failed": "Kunne ikke fjerne {{name}}",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"Czy na pewno chcesz usunąć ten {{type}}? Spowoduje to usunięcie z konfiguracji: {{scope}}.",
"marketplace.remove.cancel": "Anuluj",
"marketplace.remove.confirm.button": "Usuń",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Szukaj...",
"marketplace.filter.all": "Wszystkie elementy",
"marketplace.filter.notInstalled": "Nie zainstalowano",
"marketplace.empty": "Nie znaleziono elementów",
"marketplace.badge.mcpServer": "Serwer MCP",
"marketplace.badge.mode": "Tryb",
"marketplace.card.by": "autor: {{author}}",
"marketplace.install.method": "Metoda instalacji",
"marketplace.install.parameters": "Parametry",
"marketplace.install.optional": "(opcjonalne)",
"marketplace.install.required": "{{name}} jest wymagane",
"marketplace.scope.project": "projekt",
"marketplace.scope.global": "globalny",
"marketplace.remove.type.mcp": "serwer MCP",
"marketplace.remove.type.skill": "umiejętność",
"marketplace.remove.type.mode": "tryb",
"marketplace.remove.failed": "Nie udało się usunąć {{name}}",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"Вы уверены, что хотите удалить этот {{type}}? Это удалит его из вашей конфигурации ({{scope}}).",
"marketplace.remove.cancel": "Отмена",
"marketplace.remove.confirm.button": "Удалить",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "Поиск...",
"marketplace.filter.all": "Все элементы",
"marketplace.filter.notInstalled": "Не установлено",
"marketplace.empty": "Элементы не найдены",
"marketplace.badge.mcpServer": "MCP-сервер",
"marketplace.badge.mode": "Режим",
"marketplace.card.by": "от {{author}}",
"marketplace.install.method": "Способ установки",
"marketplace.install.parameters": "Параметры",
"marketplace.install.optional": "(необязательно)",
"marketplace.install.required": "Требуется {{name}}",
"marketplace.scope.project": "проект",
"marketplace.scope.global": "глобально",
"marketplace.remove.type.mcp": "MCP-сервер",
"marketplace.remove.type.skill": "навык",
"marketplace.remove.type.mode": "режим",
"marketplace.remove.failed": "Не удалось удалить {{name}}",
}
+18
View File
@@ -43,4 +43,22 @@ export const dict = {
"คุณแน่ใจหรือไม่ว่าต้องการลบ {{type}} นี้? สิ่งนี้จะลบออกจากการกำหนดค่า {{scope}} ของคุณ",
"marketplace.remove.cancel": "ยกเลิก",
"marketplace.remove.confirm.button": "ลบ",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "ค้นหา...",
"marketplace.filter.all": "รายการทั้งหมด",
"marketplace.filter.notInstalled": "ยังไม่ได้ติดตั้ง",
"marketplace.empty": "ไม่พบรายการ",
"marketplace.badge.mcpServer": "เซิร์ฟเวอร์ MCP",
"marketplace.badge.mode": "โหมด",
"marketplace.card.by": "โดย {{author}}",
"marketplace.install.method": "วิธีการติดตั้ง",
"marketplace.install.parameters": "พารามิเตอร์",
"marketplace.install.optional": "(ไม่บังคับ)",
"marketplace.install.required": "จำเป็นต้องใช้ {{name}}",
"marketplace.scope.project": "โปรเจ็กต์",
"marketplace.scope.global": "โกลบอล",
"marketplace.remove.type.mcp": "เซิร์ฟเวอร์ MCP",
"marketplace.remove.type.skill": "ทักษะ",
"marketplace.remove.type.mode": "โหมด",
"marketplace.remove.failed": "ไม่สามารถลบ {{name}} ได้",
}
+18
View File
@@ -40,4 +40,22 @@ export const dict = {
"marketplace.remove.confirm": "确定要移除此 {{type}} 吗?这会将其从你的 {{scope}} 配置中移除。",
"marketplace.remove.cancel": "取消",
"marketplace.remove.confirm.button": "移除",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "搜索...",
"marketplace.filter.all": "所有项目",
"marketplace.filter.notInstalled": "未安装",
"marketplace.empty": "未找到项目",
"marketplace.badge.mcpServer": "MCP 服务器",
"marketplace.badge.mode": "模式",
"marketplace.card.by": "作者:{{author}}",
"marketplace.install.method": "安装方法",
"marketplace.install.parameters": "参数",
"marketplace.install.optional": "(可选)",
"marketplace.install.required": "{{name}} 是必填项",
"marketplace.scope.project": "项目",
"marketplace.scope.global": "全局",
"marketplace.remove.type.mcp": "MCP 服务器",
"marketplace.remove.type.skill": "技能",
"marketplace.remove.type.mode": "模式",
"marketplace.remove.failed": "移除 {{name}} 失败",
}
+18
View File
@@ -40,4 +40,22 @@ export const dict = {
"marketplace.remove.confirm": "確定要移除此 {{type}} 嗎?這會將其從你的 {{scope}} 設定中移除。",
"marketplace.remove.cancel": "取消",
"marketplace.remove.confirm.button": "移除",
"marketplace.tab.mcp": "MCP",
"marketplace.search": "搜尋...",
"marketplace.filter.all": "所有項目",
"marketplace.filter.notInstalled": "未安裝",
"marketplace.empty": "找不到項目",
"marketplace.badge.mcpServer": "MCP 伺服器",
"marketplace.badge.mode": "模式",
"marketplace.card.by": "作者:{{author}}",
"marketplace.install.method": "安裝方法",
"marketplace.install.parameters": "參數",
"marketplace.install.optional": "(選填)",
"marketplace.install.required": "{{name}} 是必填項",
"marketplace.scope.project": "專案",
"marketplace.scope.global": "全域",
"marketplace.remove.type.mcp": "MCP 伺服器",
"marketplace.remove.type.skill": "技能",
"marketplace.remove.type.mode": "模式",
"marketplace.remove.failed": "移除 {{name}} 失敗",
}
+31 -2
View File
@@ -669,8 +669,11 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
case "removeInstalledMarketplaceItem": {
const workspace = this.getProjectDirectory(this.currentSession?.id)
const scope = message.mpInstallOptions?.target ?? "project"
const result = await this.getMarketplace().remove(message.mpItem, scope, workspace)
if (result.success) await this.disposeCliInstance(scope)
const result =
message.mpItem.type === "mode"
? await this.removeModeViaCli(message.mpItem)
: await this.getMarketplace().remove(message.mpItem, scope, workspace)
if (result.success && message.mpItem.type !== "mode") await this.disposeCliInstance(scope)
this.postMessage({
type: "marketplaceRemoveResult",
success: result.success,
@@ -1256,6 +1259,32 @@ export class KiloProvider implements vscode.WebviewViewProvider, TelemetryProper
await this.fetchAndSendAgents()
}
/**
* Remove a marketplace mode via the CLI removeAgent endpoint.
* Returns a RemoveResult so it plugs into the marketplace removal flow.
*/
private async removeModeViaCli(item: {
id: string
content: string
}): Promise<{ success: boolean; slug: string; error?: string }> {
if (!this.client) return { success: false, slug: item.id, error: "CLI not connected" }
const yaml = await import("yaml")
const mode = yaml.parse(item.content) as Record<string, unknown> | undefined
const slug = (mode?.slug as string) ?? item.id
try {
const dir = this.getWorkspaceDirectory()
const result = await this.client.kilocode.removeAgent({ name: slug, directory: dir })
if (result.error) {
console.error("[Kilo New] removeAgent (marketplace) returned error:", result.error)
return { success: false, slug: item.id, error: String(result.error) }
}
return { success: true, slug: item.id }
} catch (error) {
console.error("[Kilo New] Failed to remove mode (marketplace):", error)
return { success: false, slug: item.id, error: String(error) }
}
}
/**
* Dispose the CLI backend instance so it re-reads config from disk.
* Call after any marketplace install/remove that writes config files directly.
@@ -6,6 +6,9 @@ import { promisify } from "util"
import type {
MarketplaceItem,
SkillMarketplaceItem,
McpMarketplaceItem,
ModeMarketplaceItem,
McpInstallationMethod,
InstallMarketplaceItemOptions,
InstallResult,
RemoveResult,
@@ -24,9 +27,96 @@ export class MarketplaceInstaller {
): Promise<InstallResult> {
const scope = options.target ?? "project"
if (item.type === "skill") return this.installSkill(item, scope, workspace)
return { success: false, slug: item.id, error: `${item.type} installation not yet implemented` }
if (item.type === "mcp") return this.installMcp(item, options, scope, workspace)
return this.installMode(item, scope, workspace)
}
// ── MCP ─────────────────────────────────────────────────────────────
async installMcp(
item: McpMarketplaceItem,
options: InstallMarketplaceItemOptions,
scope: "project" | "global",
workspace?: string,
): Promise<InstallResult> {
if (scope === "project" && !workspace) {
return { success: false, slug: item.id, error: "No workspace directory for project-scope install" }
}
const config = await this.readConfig(scope, workspace)
if (!config.mcp) config.mcp = {}
if (config.mcp[item.id]) {
return { success: false, slug: item.id, error: "MCP server already installed. Remove it first." }
}
const content = this.resolveMcpContent(item, options)
if (!content) {
return { success: false, slug: item.id, error: "No installation content for MCP server" }
}
const entry = this.buildMcpEntry(content, options.parameters)
config.mcp[item.id] = entry
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
private resolveMcpContent(item: McpMarketplaceItem, options: InstallMarketplaceItemOptions): string | undefined {
if (typeof item.content === "string") return item.content
if (!Array.isArray(item.content) || item.content.length === 0) return undefined
const name = options.parameters?.__method as string | undefined
if (name) {
const found = item.content.find((m: McpInstallationMethod) => m.name === name)
if (found) return found.content
}
return item.content[0].content
}
private buildMcpEntry(content: string, params?: Record<string, unknown>): Record<string, unknown> {
const filtered = Object.fromEntries(Object.entries(params ?? {}).filter(([k]) => k !== "__method"))
const replaced = Object.keys(filtered).length > 0 ? substituteParams(content, filtered) : content
// Parse the content as JSON — the marketplace API provides JSON config blocks
try {
return JSON.parse(replaced)
} catch {
// Fallback: treat as a command string
return { command: replaced }
}
}
// ── Mode ────────────────────────────────────────────────────────────
async installMode(
item: ModeMarketplaceItem,
scope: "project" | "global",
workspace?: string,
): Promise<InstallResult> {
if (scope === "project" && !workspace) {
return { success: false, slug: item.id, error: "No workspace directory for project-scope install" }
}
const config = await this.readConfig(scope, workspace)
if (!config.agent) config.agent = {}
if (config.agent[item.id]) {
return { success: false, slug: item.id, error: "Mode already installed. Remove it first." }
}
// Mode content is a JSON blob for the agent config
try {
config.agent[item.id] = JSON.parse(item.content)
} catch {
config.agent[item.id] = { system: item.content }
}
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
// ── Skill ───────────────────────────────────────────────────────────
async installSkill(
item: SkillMarketplaceItem,
scope: "project" | "global",
@@ -106,9 +196,34 @@ export class MarketplaceInstaller {
}
}
// ── Remove ──────────────────────────────────────────────────────────
async remove(item: MarketplaceItem, scope: "project" | "global", workspace?: string): Promise<RemoveResult> {
if (item.type === "skill") return this.removeSkill(item, scope, workspace)
return { success: false, slug: item.id, error: `${item.type} removal not yet implemented` }
if (item.type === "mcp") return this.removeMcp(item, scope, workspace)
return this.removeMode(item, scope, workspace)
}
async removeMcp(item: McpMarketplaceItem, scope: "project" | "global", workspace?: string): Promise<RemoveResult> {
const config = await this.readConfig(scope, workspace)
if (!config.mcp?.[item.id]) {
return { success: true, slug: item.id }
}
delete config.mcp[item.id]
if (Object.keys(config.mcp).length === 0) delete config.mcp
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
async removeMode(item: ModeMarketplaceItem, scope: "project" | "global", workspace?: string): Promise<RemoveResult> {
const config = await this.readConfig(scope, workspace)
if (!config.agent?.[item.id]) {
return { success: true, slug: item.id }
}
delete config.agent[item.id]
if (Object.keys(config.agent).length === 0) delete config.agent
await this.writeConfig(scope, workspace, config)
return { success: true, slug: item.id }
}
async removeSkill(
@@ -136,6 +251,32 @@ export class MarketplaceInstaller {
return { success: false, slug: item.id, error: String(err) }
}
}
// ── Config helpers ──────────────────────────────────────────────────
private async readConfig(
scope: "project" | "global",
workspace?: string,
): Promise<Record<string, Record<string, unknown>>> {
const filepath = this.paths.configPath(scope, workspace)
try {
const content = await fs.readFile(filepath, "utf-8")
return JSON.parse(content)
} catch (err) {
if ((err as NodeJS.ErrnoException).code === "ENOENT") return {}
throw err
}
}
private async writeConfig(
scope: "project" | "global",
workspace: string | undefined,
config: Record<string, unknown>,
): Promise<void> {
const filepath = this.paths.configPath(scope, workspace)
await fs.mkdir(path.dirname(filepath), { recursive: true })
await fs.writeFile(filepath, JSON.stringify(config, null, 2) + "\n", "utf-8")
}
}
// ── Helpers ─────────────────────────────────────────────────────────
@@ -145,6 +286,15 @@ function isSafeId(id: string): boolean {
return /^[\w\-@.]+$/.test(id)
}
function substituteParams(template: string, params: Record<string, unknown>): string {
let result = template
for (const [key, value] of Object.entries(params)) {
result = result.replaceAll(`{{${key}}}`, String(value ?? ""))
result = result.replaceAll(`\${${key}}`, String(value ?? ""))
}
return result
}
async function findEscapedPaths(dir: string): Promise<string[]> {
const resolved = path.resolve(dir)
const escaped: string[] = []
@@ -1,71 +1,109 @@
import { Component, createSignal, onMount, onCleanup, Show, For } from "solid-js"
import { createSignal, createEffect, onCleanup, Show, For } from "solid-js"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import { Button } from "@kilocode/kilo-ui/button"
import { RadioGroup } from "@kilocode/kilo-ui/radio-group"
import { Select } from "@kilocode/kilo-ui/select"
import { TextField } from "@kilocode/kilo-ui/text-field"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import { useVSCode } from "../../context/vscode"
import { useServer } from "../../context/server"
import { useLanguage } from "../../context/language"
import type { MarketplaceItem } from "../../types/marketplace"
import type { MarketplaceItem, McpMarketplaceItem, McpInstallationMethod, McpParameter } from "../../types/marketplace"
interface ScopeOption {
value: "project" | "global"
label: string
disabled?: boolean
}
interface Props {
item: MarketplaceItem
onClose: () => void
onInstallResult?: (success: boolean) => void
onInstallResult: (success: boolean, scope: "project" | "global") => void
}
const InstallModal: Component<Props> = (props) => {
export const InstallModal = (props: Props) => {
const vscode = useVSCode()
const server = useServer()
const { t } = useLanguage()
const workspace = () => server.workspaceDirectory()
const scopeOptions = (): ScopeOption[] => [
{ value: "project", label: t("marketplace.install.scope.project"), disabled: !workspace() },
{ value: "global", label: t("marketplace.install.scope.global") },
const options = (): ScopeOption[] => [
{ value: "project", label: t("marketplace.scope.project") },
{ value: "global", label: t("marketplace.scope.global") },
]
const [scope, setScope] = createSignal<ScopeOption>(workspace() ? scopeOptions()[0] : scopeOptions()[1])
const initial = workspace() ? options()[0] : options()[1]
const [scope, setScope] = createSignal<ScopeOption>(initial)
const [installing, setInstalling] = createSignal(false)
const [result, setResult] = createSignal<{ success: boolean; error?: string } | null>(null)
const [params, setParams] = createSignal<Record<string, string>>({})
const prerequisites = () => props.item.prerequisites ?? []
// MCP installation methods
const methods = (): McpInstallationMethod[] => {
if (props.item.type !== "mcp") return []
const mcp = props.item as McpMarketplaceItem
if (!Array.isArray(mcp.content)) return []
return mcp.content
}
onMount(() => {
const handler = (event: MessageEvent) => {
const msg = event.data
if (msg?.type === "marketplaceInstallResult" && msg.slug === props.item.id) {
const [method, setMethod] = createSignal<McpInstallationMethod | undefined>(methods()[0])
const prerequisites = (): string[] => {
if (method()?.prerequisites?.length) return method()!.prerequisites!
return props.item.prerequisites ?? []
}
const parameters = (): McpParameter[] => {
if (method()?.parameters?.length) return method()!.parameters!
if (props.item.type === "mcp") return (props.item as McpMarketplaceItem).parameters ?? []
return []
}
const valid = () => {
for (const param of parameters()) {
if (!param.optional && !params()[param.key]?.trim()) return false
}
return true
}
const setParam = (key: string, value: string) => {
setParams((prev) => ({ ...prev, [key]: value }))
}
createEffect(() => {
const unsub = vscode.onMessage((msg) => {
if (msg.type === "marketplaceInstallResult" && msg.slug === props.item.id) {
setInstalling(false)
setResult({ success: msg.success, error: msg.error })
props.onInstallResult?.(msg.success)
props.onInstallResult(msg.success, scope().value)
}
}
window.addEventListener("message", handler)
onCleanup(() => window.removeEventListener("message", handler))
})
onCleanup(unsub)
})
const handleInstall = () => {
setInstalling(true)
const paramValues: Record<string, unknown> = { ...params() }
if (method()) {
paramValues.__method = method()!.name
}
vscode.postMessage({
type: "installMarketplaceItem",
mpItem: props.item,
mpInstallOptions: { target: scope().value },
mpInstallOptions: {
target: scope().value,
parameters: Object.keys(paramValues).length > 0 ? paramValues : undefined,
},
})
}
return (
<div class="marketplace-modal-overlay" onClick={() => !installing() && props.onClose()}>
<div class="marketplace-modal" onClick={(e) => e.stopPropagation()}>
<h3>{t("marketplace.install.title", { name: props.item.name })}</h3>
<Show when={!result()}>
<div class="marketplace-modal-section">
<label>{t("marketplace.install.scope")}</label>
<Dialog title={t("marketplace.install.title", { name: props.item.name })} fit>
<Show when={!result()}>
<div class="install-modal-body">
<div class="install-modal-section">
<span class="install-modal-label">{t("marketplace.install.scope")}</span>
<RadioGroup
options={scopeOptions()}
options={options()}
current={scope()}
value={(x: ScopeOption) => x.value}
label={(x: ScopeOption) => x.label}
@@ -73,46 +111,81 @@ const InstallModal: Component<Props> = (props) => {
/>
</div>
<Show when={methods().length > 1}>
<div class="install-modal-section">
<span class="install-modal-label">{t("marketplace.install.method")}</span>
<Select
options={methods()}
current={method()}
value={(m: McpInstallationMethod) => m.name}
label={(m: McpInstallationMethod) => m.name}
onSelect={(v: McpInstallationMethod | undefined) => v && setMethod(v)}
/>
</div>
</Show>
<Show when={prerequisites().length > 0}>
<div class="marketplace-modal-section">
<label>{t("marketplace.install.prerequisites")}</label>
<ul class="marketplace-prerequisites">
<div class="install-modal-section">
<span class="install-modal-label">{t("marketplace.install.prerequisites")}</span>
<ul class="install-modal-prerequisites">
<For each={prerequisites()}>{(p) => <li>{p}</li>}</For>
</ul>
</div>
</Show>
<div class="marketplace-modal-actions">
<Show when={parameters().length > 0}>
<div class="install-modal-section">
<span class="install-modal-label">{t("marketplace.install.parameters")}</span>
<For each={parameters()}>
{(param) => (
<div class="install-modal-param">
<TextField
label={param.name + (param.optional ? ` (${t("marketplace.install.optional")})` : "")}
placeholder={param.placeholder ?? ""}
value={params()[param.key] ?? ""}
onChange={(v: string) => setParam(param.key, v)}
/>
</div>
)}
</For>
</div>
</Show>
<div class="install-modal-footer">
<Button variant="secondary" onClick={props.onClose} disabled={installing()}>
{t("marketplace.install.cancel")}
</Button>
<Button variant="primary" onClick={handleInstall} disabled={installing()}>
{installing() ? t("marketplace.install.installing") : t("marketplace.card.install")}
<Button variant="primary" onClick={handleInstall} disabled={installing() || !valid()}>
<Show when={installing()} fallback={t("marketplace.install")}>
<Spinner /> {t("marketplace.install.installing")}
</Show>
</Button>
</div>
</Show>
</div>
</Show>
<Show when={result()}>
{(r) => (
<div class="marketplace-modal-result">
<Show
when={r().success}
fallback={
<>
<p class="marketplace-error">{r().error ?? t("marketplace.install.failed")}</p>
<Show when={result()}>
{(r) => (
<div class="install-modal-result">
<Show
when={r().success}
fallback={
<>
<p class="install-modal-error-msg">{r().error ?? t("marketplace.install.failed")}</p>
<div class="install-modal-footer">
<Button onClick={props.onClose}>{t("marketplace.install.close")}</Button>
</>
}
>
<p class="marketplace-success">{t("marketplace.install.success")}</p>
</div>
</>
}
>
<p class="install-modal-success">{t("marketplace.install.success")}</p>
<div class="install-modal-footer">
<Button onClick={props.onClose}>{t("marketplace.install.done")}</Button>
</Show>
</div>
)}
</Show>
</div>
</div>
</div>
</Show>
</div>
)}
</Show>
</Dialog>
)
}
export default InstallModal
@@ -1,7 +1,9 @@
import { Component, Show, JSX } from "solid-js"
import { Show, JSX } from "solid-js"
import { Button } from "@kilocode/kilo-ui/button"
import { Card } from "@kilocode/kilo-ui/card"
import { Tag } from "@kilocode/kilo-ui/tag"
import type { MarketplaceItem, MarketplaceInstalledMetadata } from "../../types/marketplace"
import { useVSCode } from "../../context/vscode"
import { useLanguage } from "../../context/language"
import { installedScopes } from "./utils"
@@ -16,39 +18,45 @@ interface Props {
footer?: JSX.Element
}
const ItemCard: Component<Props> = (props) => {
export const ItemCard = (props: Props) => {
const vscode = useVSCode()
const { t } = useLanguage()
const scopes = () => installedScopes(props.item.id, props.item.type, props.metadata)
const installed = () => scopes().length > 0
const name = () => props.displayName ?? props.item.name
const openLink = (url: string) => {
vscode.postMessage({ type: "openExternal", url })
}
return (
<div class="marketplace-card">
<Card class="marketplace-card">
<div class="marketplace-card-header">
<div class="marketplace-card-title">
<Show when={props.linkUrl} fallback={<span class="marketplace-card-name">{name()}</span>}>
<a
class="marketplace-card-name marketplace-card-link"
href={props.linkUrl}
target="_blank"
rel="noopener noreferrer"
>
<span class="marketplace-card-name link" onClick={() => openLink(props.linkUrl!)}>
{name()}
</a>
</span>
</Show>
<Show when={props.typeBadge}>
<Tag>{props.typeBadge}</Tag>
</Show>
</div>
<Show when={props.item.author}>
<span class="marketplace-card-author">{props.item.author}</span>
<span class="marketplace-card-author">
<Show when={props.item.authorUrl} fallback={t("marketplace.card.by", { author: props.item.author! })}>
<span class="link" onClick={() => openLink(props.item.authorUrl!)}>
{t("marketplace.card.by", { author: props.item.author! })}
</span>
</Show>
</span>
</Show>
</div>
<p class="marketplace-card-description">{props.item.description}</p>
<div class="marketplace-card-footer">
<div class="marketplace-card-tags">
<Show when={installed()}>
<Tag class="marketplace-installed-tag">{t("marketplace.card.installed")}</Tag>
<Tag class="marketplace-badge-installed">{t("marketplace.card.installed")}</Tag>
</Show>
{props.footer}
</div>
@@ -62,15 +70,18 @@ const ItemCard: Component<Props> = (props) => {
}
>
{scopes().map((scope) => (
<Button size="small" class="marketplace-remove-btn" onClick={() => props.onRemove(props.item, scope)}>
<Button
size="small"
variant="ghost"
class="marketplace-remove-btn"
onClick={() => props.onRemove(props.item, scope)}
>
{scopes().length > 1 ? t("marketplace.card.removeScope", { scope }) : t("marketplace.card.remove")}
</Button>
))}
</Show>
</div>
</div>
</div>
</Card>
)
}
export default ItemCard
@@ -0,0 +1,136 @@
import { createSignal, createMemo, 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"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import type {
MarketplaceItem,
McpMarketplaceItem,
ModeMarketplaceItem,
MarketplaceInstalledMetadata,
} from "../../types/marketplace"
import { useLanguage } from "../../context/language"
import { isInstalled } from "./utils"
import { ItemCard } from "./ItemCard"
interface StatusOption {
value: string
label: string
}
interface Props {
items: (McpMarketplaceItem | ModeMarketplaceItem)[]
metadata: MarketplaceInstalledMetadata
fetching: boolean
type: "mcp" | "mode"
searchPlaceholder: string
emptyMessage: string
onInstall: (item: MarketplaceItem) => void
onRemove: (item: MarketplaceItem, scope: "project" | "global") => void
}
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 options = (): StatusOption[] => [
{ value: "all", label: t("marketplace.filter.all") },
{ value: "installed", label: t("marketplace.filter.installed") },
{ value: "notInstalled", label: t("marketplace.filter.notInstalled") },
]
const allTags = createMemo(() => {
const set = new Set<string>()
for (const item of props.items) {
for (const tag of item.tags ?? []) set.add(tag)
}
return Array.from(set).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) => item.tags?.includes(tag))) return false
if (!q) return true
return (
item.id.toLowerCase().includes(q) ||
item.name.toLowerCase().includes(q) ||
item.description.toLowerCase().includes(q) ||
(item.author?.toLowerCase().includes(q) ?? false)
)
})
})
return (
<div class="marketplace-list">
<div class="marketplace-filters">
<div class="marketplace-search-field">
<TextField placeholder={props.searchPlaceholder} value={search()} onChange={setSearch} />
</div>
<Select
options={options()}
current={status()}
value={(o: StatusOption) => o.value}
label={(o: StatusOption) => o.label}
onSelect={(v: StatusOption | undefined) => v && setStatus(v)}
/>
</div>
<Show when={allTags().length > 0}>
<div class="marketplace-active-tags">
<For each={allTags()}>
{(tag) => (
<button
class="marketplace-tag-filter"
classList={{ active: tags().includes(tag) }}
onClick={() => toggleTag(tag)}
>
<Tag>{tag}</Tag>
</button>
)}
</For>
</div>
</Show>
<Show
when={!props.fetching}
fallback={
<div class="marketplace-loading">
<Spinner />
</div>
}
>
<Show when={filtered().length > 0} fallback={<p class="marketplace-empty">{props.emptyMessage}</p>}>
<div class="marketplace-grid">
<For each={filtered()}>
{(item) => (
<ItemCard
item={item}
metadata={props.metadata}
linkUrl={item.type === "mcp" ? (item as McpMarketplaceItem).url : undefined}
typeBadge={item.type === "mcp" ? t("marketplace.badge.mcpServer") : t("marketplace.badge.mode")}
onInstall={props.onInstall}
onRemove={props.onRemove}
footer={<For each={item.tags ?? []}>{(tag) => <Tag>{tag}</Tag>}</For>}
/>
)}
</For>
</div>
</Show>
</Show>
</div>
)
}
@@ -1,168 +1,184 @@
import { Component, createSignal, createMemo, onMount, onCleanup, Show } from "solid-js"
import { createSignal, createMemo, createEffect, onCleanup, Show } from "solid-js"
import { Tabs } from "@kilocode/kilo-ui/tabs"
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 type { MarketplaceItem, SkillMarketplaceItem, MarketplaceInstalledMetadata } from "../../types/marketplace"
import SkillsMarketplace from "./SkillsMarketplace"
import InstallModal from "./InstallModal"
import RemoveDialog from "./RemoveDialog"
import { useDialog } from "@kilocode/kilo-ui/context/dialog"
import type {
MarketplaceItem,
McpMarketplaceItem,
ModeMarketplaceItem,
SkillMarketplaceItem,
MarketplaceInstalledMetadata,
} from "../../types/marketplace"
import { SkillsMarketplace } from "./SkillsMarketplace"
import { MarketplaceListView } from "./MarketplaceListView"
import { InstallModal } from "./InstallModal"
import { RemoveDialog } from "./RemoveDialog"
import "./marketplace.css"
const EMPTY_METADATA: MarketplaceInstalledMetadata = { project: {}, global: {} }
const MarketplaceView: Component = () => {
export const MarketplaceView = () => {
const vscode = useVSCode()
const server = useServer()
const { t } = useLanguage()
const dialog = useDialog()
const [items, setItems] = createSignal<MarketplaceItem[]>([])
const [metadata, setMetadata] = createSignal<MarketplaceInstalledMetadata>(EMPTY_METADATA)
const [fetching, setFetching] = createSignal(true)
const [errors, setErrors] = createSignal<string[]>([])
const [tab, setTab] = createSignal("skill")
// Install/Remove state
const [installItem, setInstallItem] = createSignal<MarketplaceItem | null>(null)
const [removeItem, setRemoveItem] = createSignal<{ item: MarketplaceItem; scope: "project" | "global" } | null>(null)
const [pendingRemove, setPendingRemove] = createSignal<string | null>(null)
const [removeError, setRemoveError] = createSignal<string | null>(null)
const [tab, setTab] = createSignal("mcp")
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 modes = createMemo(() => items().filter((i): i is ModeMarketplaceItem => i.type === "mode"))
const fetchData = () => {
setFetching(true)
vscode.postMessage({ type: "fetchMarketplaceData" })
}
onMount(() => {
fetchData()
const handler = (event: MessageEvent) => {
const msg = event.data
if (msg?.type === "marketplaceData") {
// Listen for messages
createEffect(() => {
const unsub = vscode.onMessage((msg) => {
if (msg.type === "marketplaceData") {
setItems(msg.marketplaceItems ?? [])
setMetadata(msg.marketplaceInstalledMetadata ?? EMPTY_METADATA)
setErrors(msg.errors ?? [])
setFetching(false)
}
if (msg?.type === "marketplaceRemoveResult") {
if (msg.slug === pendingRemove()) {
setPendingRemove(null)
if (!msg.success) {
setRemoveError(msg.error ?? "Failed to remove item")
} else {
fetchData()
}
}
if (msg.type === "marketplaceRemoveResult") {
if (msg.success) fetchData()
}
// Re-fetch when agents or skills change (e.g. after install via CLI)
if (msg?.type === "agentsLoaded" || msg?.type === "skillsLoaded") {
if (msg.type === "agentsLoaded" || msg.type === "skillsLoaded") {
fetchData()
}
// Re-fetch when workspace directory changes
if (msg?.type === "workspaceDirectoryChanged") {
if (msg.type === "workspaceDirectoryChanged") {
fetchData()
}
}
window.addEventListener("message", handler)
onCleanup(() => window.removeEventListener("message", handler))
})
onCleanup(unsub)
})
// Re-fetch when workspace changes
createEffect(() => {
server.workspaceDirectory()
fetchData()
})
const telemetry = (event: string, properties?: Record<string, unknown>) => {
vscode.postMessage({ type: "telemetry", event, properties: properties ?? {} })
}
const handleInstall = (item: MarketplaceItem) => {
setInstallItem(item)
dialog.show(() => (
<InstallModal
item={item}
onClose={() => dialog.close()}
onInstallResult={(success, scope) => {
if (success) {
telemetry("marketplace_item_installed", {
id: item.id,
type: item.type,
scope,
})
dialog.close()
fetchData()
}
}}
/>
))
}
const handleRemove = (item: MarketplaceItem, scope: "project" | "global") => {
setRemoveItem({ item, scope })
dialog.show(() => (
<RemoveDialog
item={item}
scope={scope}
onClose={() => dialog.close()}
onConfirm={() => {
vscode.postMessage({
type: "removeInstalledMarketplaceItem",
mpItem: item,
mpInstallOptions: { target: scope },
})
telemetry("marketplace_item_removed", {
id: item.id,
type: item.type,
scope,
})
dialog.close()
}}
/>
))
}
const confirmRemove = () => {
const r = removeItem()
if (!r) return
setPendingRemove(r.item.id)
setRemoveError(null)
vscode.postMessage({
type: "removeInstalledMarketplaceItem",
mpItem: r.item,
mpInstallOptions: { target: r.scope },
})
setRemoveItem(null)
const dismissError = (idx: number) => {
setErrors((prev) => prev.filter((_, i) => i !== idx))
}
return (
<div class="marketplace-view">
<Tabs value={tab()} onChange={setTab}>
<Show when={errors().length > 0}>
{errors().map((err, idx) => (
<Card variant="error" class="marketplace-error-banner">
<span>{err}</span>
<Button variant="ghost" size="small" onClick={() => dismissError(idx)}>
{t("marketplace.error.dismiss")}
</Button>
</Card>
))}
</Show>
<Tabs value={tab()} onChange={setTab} class="marketplace-tabs-root">
<Tabs.List>
<Tabs.Trigger value="skill">{t("marketplace.tab.skills")}</Tabs.Trigger>
<Tabs.Trigger value="mcp">{t("marketplace.tab.mcpServers")}</Tabs.Trigger>
<Tabs.Trigger value="mcp">{t("marketplace.tab.mcp")}</Tabs.Trigger>
<Tabs.Trigger value="mode">{t("marketplace.tab.modes")}</Tabs.Trigger>
<Tabs.Trigger value="skill">{t("marketplace.tab.skills")}</Tabs.Trigger>
</Tabs.List>
<Tabs.Content value="skill">
<SkillsMarketplace
items={skills()}
metadata={metadata()}
fetching={fetching()}
onInstall={handleInstall}
onRemove={handleRemove}
/>
</Tabs.Content>
<div class="marketplace-content">
<Tabs.Content value="mcp">
<MarketplaceListView
items={mcps()}
metadata={metadata()}
fetching={fetching()}
type="mcp"
searchPlaceholder={t("marketplace.search.mcp")}
emptyMessage={t("marketplace.empty.mcp")}
onInstall={handleInstall}
onRemove={handleRemove}
/>
</Tabs.Content>
<Tabs.Content value="mcp">
<div class="marketplace-placeholder">
<p>{t("marketplace.placeholder")}</p>
</div>
</Tabs.Content>
<Tabs.Content value="mode">
<MarketplaceListView
items={modes()}
metadata={metadata()}
fetching={fetching()}
type="mode"
searchPlaceholder={t("marketplace.search.modes")}
emptyMessage={t("marketplace.empty.modes")}
onInstall={handleInstall}
onRemove={handleRemove}
/>
</Tabs.Content>
<Tabs.Content value="mode">
<div class="marketplace-placeholder">
<p>{t("marketplace.placeholder")}</p>
</div>
</Tabs.Content>
<Tabs.Content value="skill">
<SkillsMarketplace
items={skills()}
metadata={metadata()}
fetching={fetching()}
onInstall={handleInstall}
onRemove={handleRemove}
/>
</Tabs.Content>
</div>
</Tabs>
<Show when={errors().length > 0}>
<div class="marketplace-errors">
{errors().map((e) => (
<p class="marketplace-error">{e}</p>
))}
</div>
</Show>
<Show when={removeError()}>
<div class="marketplace-errors">
<p class="marketplace-error">{removeError()}</p>
</div>
</Show>
<Show when={installItem()}>
{(item) => (
<InstallModal
item={item()}
onClose={() => setInstallItem(null)}
onInstallResult={(success) => {
if (success) {
setInstallItem(null)
fetchData()
}
}}
/>
)}
</Show>
<Show when={removeItem()}>
{(r) => (
<RemoveDialog
item={r().item}
scope={r().scope}
onClose={() => setRemoveItem(null)}
onConfirm={confirmRemove}
/>
)}
</Show>
</div>
)
}
export default MarketplaceView
@@ -1,4 +1,4 @@
import { Component } from "solid-js"
import { Dialog } from "@kilocode/kilo-ui/dialog"
import { Button } from "@kilocode/kilo-ui/button"
import type { MarketplaceItem } from "../../types/marketplace"
import { useLanguage } from "../../context/language"
@@ -10,25 +10,26 @@ interface Props {
onConfirm: () => void
}
const RemoveDialog: Component<Props> = (props) => {
export const RemoveDialog = (props: Props) => {
const { t } = useLanguage()
const typeName = () => {
if (props.item.type === "mcp") return t("marketplace.type.mcp")
if (props.item.type === "mode") return t("marketplace.type.mode")
return t("marketplace.type.skill")
}
return (
<div class="marketplace-modal-overlay" onClick={props.onClose}>
<div class="marketplace-modal" onClick={(e) => e.stopPropagation()}>
<h3>{t("marketplace.remove.title", { name: props.item.name })}</h3>
<p>{t("marketplace.remove.confirm", { type: props.item.type, scope: props.scope })}</p>
<div class="marketplace-modal-actions">
<Button variant="secondary" onClick={props.onClose}>
{t("marketplace.remove.cancel")}
</Button>
<Button variant="primary" class="marketplace-remove-btn" onClick={props.onConfirm}>
{t("marketplace.remove.confirm.button")}
</Button>
</div>
<Dialog title={t("marketplace.remove.title", { name: props.item.name })} fit>
<p>{t("marketplace.remove.confirm", { type: typeName(), scope: t(`marketplace.scope.${props.scope}`) })}</p>
<div class="marketplace-remove-actions">
<Button variant="secondary" onClick={props.onClose}>
{t("marketplace.remove.cancel")}
</Button>
<Button variant="primary" class="danger-btn" onClick={props.onConfirm}>
{t("marketplace.remove.confirm.button")}
</Button>
</div>
</div>
</Dialog>
)
}
export default RemoveDialog
@@ -1,11 +1,11 @@
import { Component, createSignal, createMemo, For, Show } from "solid-js"
import { createSignal, createMemo, For, Show } from "solid-js"
import { TextField } from "@kilocode/kilo-ui/text-field"
import { RadioGroup } from "@kilocode/kilo-ui/radio-group"
import { Tag } from "@kilocode/kilo-ui/tag"
import { Spinner } from "@kilocode/kilo-ui/spinner"
import type { SkillMarketplaceItem, MarketplaceInstalledMetadata, MarketplaceItem } from "../../types/marketplace"
import { useLanguage } from "../../context/language"
import ItemCard from "./ItemCard"
import { ItemCard } from "./ItemCard"
interface Props {
items: SkillMarketplaceItem[]
@@ -17,7 +17,7 @@ interface Props {
const ALL = "__all__"
const SkillsMarketplace: Component<Props> = (props) => {
export const SkillsMarketplace = (props: Props) => {
const { t } = useLanguage()
const [search, setSearch] = createSignal("")
const [category, setCategory] = createSignal(ALL)
@@ -82,5 +82,3 @@ const SkillsMarketplace: Component<Props> = (props) => {
</div>
)
}
export default SkillsMarketplace
@@ -1 +1 @@
export { default as MarketplaceView } from "./MarketplaceView"
export { MarketplaceView } from "./MarketplaceView"
@@ -6,6 +6,19 @@
height: 100%;
padding: 12px;
overflow: auto;
gap: 8px;
}
.marketplace-tabs-root {
display: flex;
flex-direction: column;
flex: 1;
min-height: 0;
}
.marketplace-content {
flex: 1;
overflow: auto;
}
/* List layout */
@@ -23,10 +36,52 @@
align-items: center;
}
.marketplace-search-field {
flex: 1;
}
.marketplace-category-filters {
overflow-x: auto;
}
/* Tag filters */
.marketplace-active-tags {
display: flex;
flex-wrap: wrap;
gap: 4px;
}
.marketplace-tag-filter {
all: unset;
cursor: pointer;
opacity: 0.5;
transition: opacity 0.15s ease;
}
.marketplace-tag-filter:hover {
opacity: 0.8;
}
.marketplace-tag-filter.active {
opacity: 1;
}
/* Loading & empty */
.marketplace-loading {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
}
.marketplace-empty {
text-align: center;
color: var(--text-weak);
padding: 24px;
}
/* Grid */
.marketplace-grid {
@@ -48,9 +103,6 @@
flex-direction: column;
gap: 8px;
padding: 12px;
border: 1px solid var(--border-base);
border-radius: 6px;
background: var(--background-base);
}
.marketplace-card-header {
@@ -71,14 +123,6 @@
color: var(--text-base);
}
.marketplace-card-link {
text-decoration: none;
}
.marketplace-card-link:hover {
text-decoration: underline;
}
.marketplace-card-author {
font-size: 12px;
color: var(--text-weak);
@@ -115,117 +159,109 @@
flex-shrink: 0;
}
.marketplace-installed-tag {
/* Installed badge */
.marketplace-badge-installed {
color: var(--text-on-success-base) !important;
border-color: var(--border-success-base) !important;
}
/* Remove button */
.marketplace-remove-btn {
color: var(--text-on-critical-base) !important;
border-color: var(--border-critical-base) !important;
}
/* Placeholder */
/* Remove dialog actions */
.marketplace-placeholder {
display: flex;
justify-content: center;
align-items: center;
min-height: 200px;
color: var(--text-weak);
font-size: 16px;
}
/* Empty state */
.marketplace-empty {
text-align: center;
color: var(--text-weak);
padding: 24px;
}
/* Modal / Dialog */
.marketplace-modal-overlay {
position: fixed;
top: 0;
left: 0;
right: 0;
bottom: 0;
background: rgba(0, 0, 0, 0.5);
display: flex;
align-items: center;
justify-content: center;
z-index: 1000;
}
.marketplace-modal {
background: var(--background-base);
border: 1px solid var(--border-base);
border-radius: 8px;
padding: 20px;
max-width: 480px;
width: 90%;
max-height: 80vh;
overflow-y: auto;
}
.marketplace-modal h3 {
margin: 0 0 16px;
font-size: 16px;
color: var(--text-base);
}
.marketplace-modal p {
margin: 0 0 12px;
color: var(--text-weak);
font-size: 13px;
}
.marketplace-modal-section {
margin-bottom: 16px;
}
.marketplace-modal-section > label {
display: block;
font-weight: 600;
font-size: 13px;
margin-bottom: 6px;
color: var(--text-base);
}
.marketplace-modal-actions {
.marketplace-remove-actions {
display: flex;
justify-content: flex-end;
gap: 8px;
margin-top: 16px;
}
.marketplace-modal-result {
text-align: center;
padding: 12px 0;
/* Danger button (remove confirm) */
.danger-btn {
color: var(--text-on-critical-base) !important;
border-color: var(--border-critical-base) !important;
}
.marketplace-prerequisites {
/* Install modal */
.install-modal-body {
display: flex;
flex-direction: column;
gap: 16px;
}
.install-modal-section {
display: flex;
flex-direction: column;
gap: 6px;
}
.install-modal-label {
font-weight: 600;
font-size: 13px;
color: var(--text-base);
}
.install-modal-prerequisites {
margin: 0;
padding-left: 20px;
font-size: 13px;
color: var(--text-weak);
}
/* Error/Success messages */
.marketplace-errors {
margin-top: 12px;
.install-modal-param {
margin-bottom: 8px;
}
.marketplace-error {
color: var(--text-on-critical-base);
font-size: 13px;
.install-modal-footer {
display: flex;
justify-content: flex-end;
gap: 8px;
}
.marketplace-success {
.install-modal-result {
text-align: center;
padding: 12px 0;
}
.install-modal-success {
color: var(--text-on-success-base);
font-size: 14px;
font-weight: 600;
margin: 0 0 16px;
}
.install-modal-error-msg {
color: var(--text-on-critical-base);
font-size: 13px;
margin: 0 0 16px;
}
/* Error banner */
.marketplace-error-banner {
display: flex;
align-items: center;
justify-content: space-between;
gap: 8px;
padding: 8px 12px;
font-size: 13px;
}
/* Link style */
.link {
color: var(--text-interactive-base);
cursor: pointer;
}
.link:hover {
text-decoration: underline;
}
@@ -1,11 +1,19 @@
import type { MarketplaceInstalledMetadata } from "../../types/marketplace"
export function isInstalled(
id: string,
type: string,
metadata: MarketplaceInstalledMetadata,
): "project" | "global" | false {
return installedScopes(id, type, metadata)[0] ?? false
}
export function installedScopes(
id: string,
type: string,
metadata: MarketplaceInstalledMetadata,
): Array<"project" | "global"> {
const scopes: Array<"project" | "global"> = []
): ("project" | "global")[] {
const scopes: ("project" | "global")[] = []
if (metadata.project[id]?.type === type) scopes.push("project")
if (metadata.global[id]?.type === type) scopes.push("global")
return scopes
@@ -1,3 +1,17 @@
export interface McpParameter {
name: string
key: string
placeholder?: string
optional?: boolean
}
export interface McpInstallationMethod {
name: string
content: string
parameters?: McpParameter[]
prerequisites?: string[]
}
export interface MarketplaceItemBase {
id: string
name: string
@@ -11,7 +25,8 @@ export interface MarketplaceItemBase {
export interface McpMarketplaceItem extends MarketplaceItemBase {
type: "mcp"
url: string
content: string | unknown[]
content: string | McpInstallationMethod[]
parameters?: McpParameter[]
}
export interface ModeMarketplaceItem extends MarketplaceItemBase {
@@ -41,8 +56,7 @@ export interface MarketplaceInstalledMetadata {
}
export interface MarketplaceFilters {
search?: string
type?: string
status?: string
search?: string
tags?: string[]
}