From 0c92a892aececcdfe7515e8cc2de1ae383d479ba Mon Sep 17 00:00:00 2001 From: Mark IJbema Date: Mon, 16 Mar 2026 19:27:51 +0100 Subject: [PATCH] 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). --- packages/kilo-i18n/src/ar.ts | 18 ++ packages/kilo-i18n/src/br.ts | 18 ++ packages/kilo-i18n/src/bs.ts | 18 ++ packages/kilo-i18n/src/da.ts | 18 ++ packages/kilo-i18n/src/de.ts | 18 ++ packages/kilo-i18n/src/en.ts | 18 ++ packages/kilo-i18n/src/es.ts | 18 ++ packages/kilo-i18n/src/fr.ts | 18 ++ packages/kilo-i18n/src/ja.ts | 18 ++ packages/kilo-i18n/src/ko.ts | 18 ++ packages/kilo-i18n/src/no.ts | 18 ++ packages/kilo-i18n/src/pl.ts | 18 ++ packages/kilo-i18n/src/ru.ts | 18 ++ packages/kilo-i18n/src/th.ts | 18 ++ packages/kilo-i18n/src/zh.ts | 18 ++ packages/kilo-i18n/src/zht.ts | 18 ++ packages/kilo-vscode/src/KiloProvider.ts | 33 ++- .../src/services/marketplace/installer.ts | 154 ++++++++++- .../components/marketplace/InstallModal.tsx | 181 +++++++++---- .../src/components/marketplace/ItemCard.tsx | 43 ++-- .../marketplace/MarketplaceListView.tsx | 136 ++++++++++ .../marketplace/MarketplaceView.tsx | 240 ++++++++++-------- .../components/marketplace/RemoveDialog.tsx | 35 +-- .../marketplace/SkillsMarketplace.tsx | 8 +- .../src/components/marketplace/index.ts | 2 +- .../components/marketplace/marketplace.css | 222 +++++++++------- .../src/components/marketplace/utils.ts | 12 +- .../webview-ui/src/types/marketplace.ts | 20 +- 28 files changed, 1067 insertions(+), 307 deletions(-) create mode 100644 packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceListView.tsx diff --git a/packages/kilo-i18n/src/ar.ts b/packages/kilo-i18n/src/ar.ts index 4abab3098f2..6bd89a624c6 100644 --- a/packages/kilo-i18n/src/ar.ts +++ b/packages/kilo-i18n/src/ar.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/br.ts b/packages/kilo-i18n/src/br.ts index cfa23e214d6..6acda4e7942 100644 --- a/packages/kilo-i18n/src/br.ts +++ b/packages/kilo-i18n/src/br.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/bs.ts b/packages/kilo-i18n/src/bs.ts index aecc7423546..a2a8be0ed1f 100644 --- a/packages/kilo-i18n/src/bs.ts +++ b/packages/kilo-i18n/src/bs.ts @@ -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", } diff --git a/packages/kilo-i18n/src/da.ts b/packages/kilo-i18n/src/da.ts index fb5541bdaeb..8c5ff923725 100644 --- a/packages/kilo-i18n/src/da.ts +++ b/packages/kilo-i18n/src/da.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/de.ts b/packages/kilo-i18n/src/de.ts index bd34499ade6..0ee1c8bd0c7 100644 --- a/packages/kilo-i18n/src/de.ts +++ b/packages/kilo-i18n/src/de.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/en.ts b/packages/kilo-i18n/src/en.ts index 5cccd33c5c2..d8ae2dee47d 100644 --- a/packages/kilo-i18n/src/en.ts +++ b/packages/kilo-i18n/src/en.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/es.ts b/packages/kilo-i18n/src/es.ts index cdedb7864b8..570e93f2672 100644 --- a/packages/kilo-i18n/src/es.ts +++ b/packages/kilo-i18n/src/es.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/fr.ts b/packages/kilo-i18n/src/fr.ts index e741db9adc7..545f324f9e8 100644 --- a/packages/kilo-i18n/src/fr.ts +++ b/packages/kilo-i18n/src/fr.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/ja.ts b/packages/kilo-i18n/src/ja.ts index 460697114ab..c368f713a5e 100644 --- a/packages/kilo-i18n/src/ja.ts +++ b/packages/kilo-i18n/src/ja.ts @@ -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}} の削除に失敗しました", } diff --git a/packages/kilo-i18n/src/ko.ts b/packages/kilo-i18n/src/ko.ts index 1d2d11e4443..857dd478ea6 100644 --- a/packages/kilo-i18n/src/ko.ts +++ b/packages/kilo-i18n/src/ko.ts @@ -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}} 제거 실패", } diff --git a/packages/kilo-i18n/src/no.ts b/packages/kilo-i18n/src/no.ts index 70e5f3a3e0c..6878d64b0c3 100644 --- a/packages/kilo-i18n/src/no.ts +++ b/packages/kilo-i18n/src/no.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/pl.ts b/packages/kilo-i18n/src/pl.ts index b4b7f0685a1..1cc4c5f420d 100644 --- a/packages/kilo-i18n/src/pl.ts +++ b/packages/kilo-i18n/src/pl.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/ru.ts b/packages/kilo-i18n/src/ru.ts index 581b6925967..cf8a06e54a0 100644 --- a/packages/kilo-i18n/src/ru.ts +++ b/packages/kilo-i18n/src/ru.ts @@ -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}}", } diff --git a/packages/kilo-i18n/src/th.ts b/packages/kilo-i18n/src/th.ts index 5fe5514aab0..196fbbecca9 100644 --- a/packages/kilo-i18n/src/th.ts +++ b/packages/kilo-i18n/src/th.ts @@ -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}} ได้", } diff --git a/packages/kilo-i18n/src/zh.ts b/packages/kilo-i18n/src/zh.ts index a5f9d8870d3..3ddf2b8c988 100644 --- a/packages/kilo-i18n/src/zh.ts +++ b/packages/kilo-i18n/src/zh.ts @@ -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}} 失败", } diff --git a/packages/kilo-i18n/src/zht.ts b/packages/kilo-i18n/src/zht.ts index 219ed9d7f95..ff74dab6e2f 100644 --- a/packages/kilo-i18n/src/zht.ts +++ b/packages/kilo-i18n/src/zht.ts @@ -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}} 失敗", } diff --git a/packages/kilo-vscode/src/KiloProvider.ts b/packages/kilo-vscode/src/KiloProvider.ts index 09e98a3cda2..f8273f964a2 100644 --- a/packages/kilo-vscode/src/KiloProvider.ts +++ b/packages/kilo-vscode/src/KiloProvider.ts @@ -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 | 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. diff --git a/packages/kilo-vscode/src/services/marketplace/installer.ts b/packages/kilo-vscode/src/services/marketplace/installer.ts index 7cf55351ef5..0b19073be49 100644 --- a/packages/kilo-vscode/src/services/marketplace/installer.ts +++ b/packages/kilo-vscode/src/services/marketplace/installer.ts @@ -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 { 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 { + 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): Record { + 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 { + 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 { 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 { + 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 { + 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>> { + 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, + ): Promise { + 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 { + 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 { const resolved = path.resolve(dir) const escaped: string[] = [] diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx index e070513b5de..3655739c762 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/InstallModal.tsx @@ -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) => { +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(workspace() ? scopeOptions()[0] : scopeOptions()[1]) + const initial = workspace() ? options()[0] : options()[1] + const [scope, setScope] = createSignal(initial) const [installing, setInstalling] = createSignal(false) const [result, setResult] = createSignal<{ success: boolean; error?: string } | null>(null) + const [params, setParams] = createSignal>({}) - 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(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 = { ...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 ( -
!installing() && props.onClose()}> -
e.stopPropagation()}> -

{t("marketplace.install.title", { name: props.item.name })}

- - -
- + + +
+
+ {t("marketplace.install.scope")} x.value} label={(x: ScopeOption) => x.label} @@ -73,46 +111,81 @@ const InstallModal: Component = (props) => { />
+ 1}> +
+ {t("marketplace.install.method")} + o.value} + label={(o: StatusOption) => o.label} + onSelect={(v: StatusOption | undefined) => v && setStatus(v)} + /> +
+ 0}> +
+ + {(tag) => ( + + )} + +
+
+ + +
+ } + > + 0} fallback={

{props.emptyMessage}

}> +
+ + {(item) => ( + {(tag) => {tag}}} + /> + )} + +
+
+
+
+ ) +} diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx index e821a6e0beb..7e546aad7b9 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/MarketplaceView.tsx @@ -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([]) const [metadata, setMetadata] = createSignal(EMPTY_METADATA) const [fetching, setFetching] = createSignal(true) const [errors, setErrors] = createSignal([]) - const [tab, setTab] = createSignal("skill") - - // Install/Remove state - const [installItem, setInstallItem] = createSignal(null) - const [removeItem, setRemoveItem] = createSignal<{ item: MarketplaceItem; scope: "project" | "global" } | null>(null) - const [pendingRemove, setPendingRemove] = createSignal(null) - const [removeError, setRemoveError] = createSignal(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) => { + vscode.postMessage({ type: "telemetry", event, properties: properties ?? {} }) + } + const handleInstall = (item: MarketplaceItem) => { - setInstallItem(item) + dialog.show(() => ( + 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(() => ( + 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 (
- + 0}> + {errors().map((err, idx) => ( + + {err} + + + ))} + + + - {t("marketplace.tab.skills")} - {t("marketplace.tab.mcpServers")} + {t("marketplace.tab.mcp")} {t("marketplace.tab.modes")} + {t("marketplace.tab.skills")} - - - +
+ + + - -
-

{t("marketplace.placeholder")}

-
-
+ + + - -
-

{t("marketplace.placeholder")}

-
-
+ + + +
- - 0}> -
- {errors().map((e) => ( -

{e}

- ))} -
-
- - -
-

{removeError()}

-
-
- - - {(item) => ( - setInstallItem(null)} - onInstallResult={(success) => { - if (success) { - setInstallItem(null) - fetchData() - } - }} - /> - )} - - - - {(r) => ( - setRemoveItem(null)} - onConfirm={confirmRemove} - /> - )} -
) } - -export default MarketplaceView diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx index 4e544fac0cd..b092861b90e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/RemoveDialog.tsx @@ -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) => { +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 ( -
-
e.stopPropagation()}> -

{t("marketplace.remove.title", { name: props.item.name })}

-

{t("marketplace.remove.confirm", { type: props.item.type, scope: props.scope })}

-
- - -
+ +

{t("marketplace.remove.confirm", { type: typeName(), scope: t(`marketplace.scope.${props.scope}`) })}

+
+ +
-
+ ) } - -export default RemoveDialog diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx b/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx index e55c4d3419c..d71a250266e 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/SkillsMarketplace.tsx @@ -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) => { +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) => {
) } - -export default SkillsMarketplace diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts b/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts index 979b85151d1..e322a99a16f 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/index.ts @@ -1 +1 @@ -export { default as MarketplaceView } from "./MarketplaceView" +export { MarketplaceView } from "./MarketplaceView" diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css index 49c95541ce4..5d958d7fdd2 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/marketplace.css @@ -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; } diff --git a/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts b/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts index 264b722a54c..9504e734f37 100644 --- a/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts +++ b/packages/kilo-vscode/webview-ui/src/components/marketplace/utils.ts @@ -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 diff --git a/packages/kilo-vscode/webview-ui/src/types/marketplace.ts b/packages/kilo-vscode/webview-ui/src/types/marketplace.ts index d41a7351266..943d2bec10a 100644 --- a/packages/kilo-vscode/webview-ui/src/types/marketplace.ts +++ b/packages/kilo-vscode/webview-ui/src/types/marketplace.ts @@ -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[] }