feat: add confirmation dialog and proper cleanup for marketplace mode removal (#6136)

Co-authored-by: Daniel Riccio <ricciodaniel98@gmail.com>
This commit is contained in:
Hannes Rudolph 2025-07-23 22:57:10 -06:00 committed by GitHub
parent 3977bbc0ce
commit 1e17b3b3bb
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
44 changed files with 887 additions and 277 deletions

View file

@ -1,6 +1,7 @@
import * as vscode from "vscode"
import * as path from "path"
import * as fs from "fs/promises"
import * as os from "os"
import * as yaml from "yaml"
import stripBom from "strip-bom"
@ -501,7 +502,7 @@ export class CustomModesManager {
await this.onUpdate()
}
public async deleteCustomMode(slug: string): Promise<void> {
public async deleteCustomMode(slug: string, fromMarketplace = false): Promise<void> {
try {
const settingsPath = await this.getCustomModesFilePath()
const roomodesPath = await this.getWorkspaceRoomodes()
@ -517,6 +518,9 @@ export class CustomModesManager {
throw new Error(t("common:customModes.errors.modeNotFound"))
}
// Determine which mode to use for rules folder path calculation
const modeToDelete = projectMode || globalMode
await this.queueWrite(async () => {
// Delete from project first if it exists there
if (projectMode && roomodesPath) {
@ -528,6 +532,11 @@ export class CustomModesManager {
await this.updateModesInFile(settingsPath, (modes) => modes.filter((m) => m.slug !== slug))
}
// Delete associated rules folder
if (modeToDelete) {
await this.deleteRulesFolder(slug, modeToDelete, fromMarketplace)
}
// Clear cache when modes are deleted
this.clearCache()
await this.refreshMergedState()
@ -538,6 +547,54 @@ export class CustomModesManager {
}
}
/**
* Deletes the rules folder for a specific mode
* @param slug - The mode slug
* @param mode - The mode configuration to determine the scope
*/
private async deleteRulesFolder(slug: string, mode: ModeConfig, fromMarketplace = false): Promise<void> {
try {
// Determine the scope based on source (project or global)
const scope = mode.source || "global"
// Determine the rules folder path
let rulesFolderPath: string
if (scope === "project") {
const workspacePath = getWorkspacePath()
if (workspacePath) {
rulesFolderPath = path.join(workspacePath, ".roo", `rules-${slug}`)
} else {
return // No workspace, can't delete project rules
}
} else {
// Global scope - use OS home directory
const homeDir = os.homedir()
rulesFolderPath = path.join(homeDir, ".roo", `rules-${slug}`)
}
// Check if the rules folder exists and delete it
const rulesFolderExists = await fileExistsAtPath(rulesFolderPath)
if (rulesFolderExists) {
try {
await fs.rm(rulesFolderPath, { recursive: true, force: true })
logger.info(`Deleted rules folder for mode ${slug}: ${rulesFolderPath}`)
} catch (error) {
logger.error(`Failed to delete rules folder for mode ${slug}: ${error}`)
// Notify the user about the failure
const messageKey = fromMarketplace
? "common:marketplace.mode.rulesCleanupFailed"
: "common:customModes.errors.rulesCleanupFailed"
vscode.window.showWarningMessage(t(messageKey, { rulesFolderPath }))
// Continue even if folder deletion fails
}
}
} catch (error) {
logger.error(`Error deleting rules folder for mode ${slug}`, {
error: error instanceof Error ? error.message : String(error),
})
}
}
public async resetCustomModes(): Promise<void> {
try {
const filePath = await this.getCustomModesFilePath()

View file

@ -158,7 +158,7 @@ export class ClineProvider
this.log(`Failed to initialize MCP Hub: ${error}`)
})
this.marketplaceManager = new MarketplaceManager(this.context)
this.marketplaceManager = new MarketplaceManager(this.context, this.customModesManager)
}
// Adds a new Cline instance to clineStack, marking the start of a new task.

View file

@ -2229,8 +2229,45 @@ export const webviewMessageHandler = async (
try {
await marketplaceManager.removeInstalledMarketplaceItem(message.mpItem, message.mpInstallOptions)
await provider.postStateToWebview()
// Send success message to webview
provider.postMessageToWebview({
type: "marketplaceRemoveResult",
success: true,
slug: message.mpItem.id,
})
} catch (error) {
console.error(`Error removing marketplace item: ${error}`)
// Show error message to user
vscode.window.showErrorMessage(
`Failed to remove marketplace item: ${error instanceof Error ? error.message : String(error)}`,
)
// Send error message to webview
provider.postMessageToWebview({
type: "marketplaceRemoveResult",
success: false,
error: error instanceof Error ? error.message : String(error),
slug: message.mpItem.id,
})
}
} else {
// MarketplaceManager not available or missing required parameters
const errorMessage = !marketplaceManager
? "Marketplace manager is not available"
: "Missing required parameters for marketplace item removal"
console.error(errorMessage)
vscode.window.showErrorMessage(errorMessage)
if (message.mpItem?.id) {
provider.postMessageToWebview({
type: "marketplaceRemoveResult",
success: false,
error: errorMessage,
slug: message.mpItem.id,
})
}
}
break

View file

@ -147,13 +147,19 @@
"deleteFailed": "Error en eliminar el mode personalitzat: {{error}}",
"resetFailed": "Error en restablir els modes personalitzats: {{error}}",
"modeNotFound": "Error d'escriptura: Mode no trobat",
"noWorkspaceForProject": "No s'ha trobat cap carpeta d'espai de treball per al mode específic del projecte"
"noWorkspaceForProject": "No s'ha trobat cap carpeta d'espai de treball per al mode específic del projecte",
"rulesCleanupFailed": "El mode s'ha suprimit correctament, però no s'ha pogut suprimir la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis de suprimir manualment."
},
"scope": {
"project": "projecte",
"global": "global"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "El mode s'ha eliminat correctament, però no s'ha pogut eliminar la carpeta de regles a {{rulesFolderPath}}. És possible que l'hagis d'eliminar manualment."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "La teva organització requereix autenticació de Roo Code Cloud. Si us plau, inicia sessió per continuar.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Fehler beim Löschen des benutzerdefinierten Modus: {{error}}",
"resetFailed": "Fehler beim Zurücksetzen der benutzerdefinierten Modi: {{error}}",
"modeNotFound": "Schreibfehler: Modus nicht gefunden",
"noWorkspaceForProject": "Kein Arbeitsbereich-Ordner für projektspezifischen Modus gefunden"
"noWorkspaceForProject": "Kein Arbeitsbereich-Ordner für projektspezifischen Modus gefunden",
"rulesCleanupFailed": "Der Modus wurde erfolgreich gelöscht, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen."
},
"scope": {
"project": "projekt",
"global": "global"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Der Modus wurde erfolgreich entfernt, aber der Regelordner unter {{rulesFolderPath}} konnte nicht gelöscht werden. Möglicherweise musst du ihn manuell löschen."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Deine Organisation erfordert eine Roo Code Cloud-Authentifizierung. Bitte melde dich an, um fortzufahren.",

View file

@ -136,13 +136,19 @@
"deleteFailed": "Failed to delete custom mode: {{error}}",
"resetFailed": "Failed to reset custom modes: {{error}}",
"modeNotFound": "Write error: Mode not found",
"noWorkspaceForProject": "No workspace folder found for project-specific mode"
"noWorkspaceForProject": "No workspace folder found for project-specific mode",
"rulesCleanupFailed": "Mode deleted successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually."
},
"scope": {
"project": "project",
"global": "global"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Mode removed successfully, but failed to delete rules folder at {{rulesFolderPath}}. You may need to delete it manually."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Your organization requires Roo Code Cloud authentication. Please sign in to continue.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Error al eliminar modo personalizado: {{error}}",
"resetFailed": "Error al restablecer modos personalizados: {{error}}",
"modeNotFound": "Error de escritura: Modo no encontrado",
"noWorkspaceForProject": "No se encontró carpeta de espacio de trabajo para modo específico del proyecto"
"noWorkspaceForProject": "No se encontró carpeta de espacio de trabajo para modo específico del proyecto",
"rulesCleanupFailed": "El modo se eliminó correctamente, pero no se pudo eliminar la carpeta de reglas en {{rulesFolderPath}}. Es posible que debas eliminarla manualmente."
},
"scope": {
"project": "proyecto",
"global": "global"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "El modo se eliminó correctamente, pero no se pudo eliminar la carpeta de reglas en {{rulesFolderPath}}. Es posible que debas eliminarla manually."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Tu organización requiere autenticación de Roo Code Cloud. Por favor, inicia sesión para continuar.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Échec de la suppression du mode personnalisé : {{error}}",
"resetFailed": "Échec de la réinitialisation des modes personnalisés : {{error}}",
"modeNotFound": "Erreur d'écriture : Mode non trouvé",
"noWorkspaceForProject": "Aucun dossier d'espace de travail trouvé pour le mode spécifique au projet"
"noWorkspaceForProject": "Aucun dossier d'espace de travail trouvé pour le mode spécifique au projet",
"rulesCleanupFailed": "Le mode a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement."
},
"scope": {
"project": "projet",
"global": "global"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Le mode a été supprimé avec succès, mais la suppression du dossier de règles à l'adresse {{rulesFolderPath}} a échoué. Vous devrez peut-être le supprimer manuellement."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Votre organisation nécessite une authentification Roo Code Cloud. Veuillez vous connecter pour continuer.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "कस्टम मोड डिलीट विफल: {{error}}",
"resetFailed": "कस्टम मोड रीसेट विफल: {{error}}",
"modeNotFound": "लेखन त्रुटि: मोड नहीं मिला",
"noWorkspaceForProject": "प्रोजेक्ट-विशिष्ट मोड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला"
"noWorkspaceForProject": "प्रोजेक्ट-विशिष्ट मोड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला",
"rulesCleanupFailed": "मोड सफलतापूर्वक हटा दिया गया, लेकिन {{rulesFolderPath}} पर नियम फ़ोल्डर को हटाने में विफल रहा। आपको इसे मैन्युअल रूप से हटाना पड़ सकता है।"
},
"scope": {
"project": "परियोजना",
"global": "वैश्विक"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "मोड सफलतापूर्वक हटा दिया गया, लेकिन {{rulesFolderPath}} पर नियम फ़ोल्डर को हटाने में विफल रहा। आपको इसे मैन्युअल रूप से हटाना पड़ सकता है।"
}
},
"mdm": {
"errors": {
"cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Gagal menghapus mode kustom: {{error}}",
"resetFailed": "Gagal mereset mode kustom: {{error}}",
"modeNotFound": "Kesalahan tulis: Mode tidak ditemukan",
"noWorkspaceForProject": "Tidak ditemukan folder workspace untuk mode khusus proyek"
"noWorkspaceForProject": "Tidak ditemukan folder workspace untuk mode khusus proyek",
"rulesCleanupFailed": "Mode berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual."
},
"scope": {
"project": "proyek",
"global": "global"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Mode berhasil dihapus, tetapi gagal menghapus folder aturan di {{rulesFolderPath}}. Kamu mungkin perlu menghapusnya secara manual."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Eliminazione modalità personalizzata fallita: {{error}}",
"resetFailed": "Reset modalità personalizzate fallito: {{error}}",
"modeNotFound": "Errore di scrittura: Modalità non trovata",
"noWorkspaceForProject": "Nessuna cartella workspace trovata per la modalità specifica del progetto"
"noWorkspaceForProject": "Nessuna cartella workspace trovata per la modalità specifica del progetto",
"rulesCleanupFailed": "La modalità è stata eliminata con successo, ma non è stato possibile eliminare la cartella delle regole in {{rulesFolderPath}}. Potrebbe essere necessario eliminarla manualmente."
},
"scope": {
"project": "progetto",
"global": "globale"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "La modalità è stata rimossa con successo, ma non è stato possibile eliminare la cartella delle regole in {{rulesFolderPath}}. Potrebbe essere necessario eliminarla manualmente."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "La tua organizzazione richiede l'autenticazione Roo Code Cloud. Accedi per continuare.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "カスタムモードの削除に失敗しました:{{error}}",
"resetFailed": "カスタムモードのリセットに失敗しました:{{error}}",
"modeNotFound": "書き込みエラー:モードが見つかりません",
"noWorkspaceForProject": "プロジェクト固有モード用のワークスペースフォルダーが見つかりません"
"noWorkspaceForProject": "プロジェクト固有モード用のワークスペースフォルダーが見つかりません",
"rulesCleanupFailed": "モードは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。"
},
"scope": {
"project": "プロジェクト",
"global": "グローバル"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "モードは正常に削除されましたが、{{rulesFolderPath}} にあるルールフォルダの削除に失敗しました。手動で削除する必要がある場合があります。"
}
},
"mdm": {
"errors": {
"cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。",

View file

@ -147,13 +147,19 @@
"deleteFailed": "사용자 정의 모드 삭제 실패: {{error}}",
"resetFailed": "사용자 정의 모드 재설정 실패: {{error}}",
"modeNotFound": "쓰기 오류: 모드를 찾을 수 없습니다",
"noWorkspaceForProject": "프로젝트별 모드용 작업 공간 폴더를 찾을 수 없습니다"
"noWorkspaceForProject": "프로젝트별 모드용 작업 공간 폴더를 찾을 수 없습니다",
"rulesCleanupFailed": "모드가 성공적으로 삭제되었지만 {{rulesFolderPath}}의 규칙 폴더를 삭제하지 못했습니다. 수동으로 삭제해야 할 수도 있습니다."
},
"scope": {
"project": "프로젝트",
"global": "글로벌"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "모드가 성공적으로 제거되었지만 {{rulesFolderPath}}의 규칙 폴더를 삭제하지 못했습니다. 수동으로 삭제해야 할 수도 있습니다."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Aangepaste modus verwijderen mislukt: {{error}}",
"resetFailed": "Aangepaste modi resetten mislukt: {{error}}",
"modeNotFound": "Schrijffout: Modus niet gevonden",
"noWorkspaceForProject": "Geen workspace map gevonden voor projectspecifieke modus"
"noWorkspaceForProject": "Geen workspace map gevonden voor projectspecifieke modus",
"rulesCleanupFailed": "Modus succesvol verwijderd, maar het verwijderen van de regelsmap op {{rulesFolderPath}} is mislukt. Je moet deze mogelijk handmatig verwijderen."
},
"scope": {
"project": "project",
"global": "globaal"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Modus succesvol verwijderd, maar het verwijderen van de regelsmap op {{rulesFolderPath}} is mislukt. Je moet deze mogelijk handmatig verwijderen."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Usunięcie trybu niestandardowego nie powiodło się: {{error}}",
"resetFailed": "Resetowanie trybów niestandardowych nie powiodło się: {{error}}",
"modeNotFound": "Błąd zapisu: Tryb nie został znaleziony",
"noWorkspaceForProject": "Nie znaleziono folderu obszaru roboczego dla trybu specyficznego dla projektu"
"noWorkspaceForProject": "Nie znaleziono folderu obszaru roboczego dla trybu specyficznego dla projektu",
"rulesCleanupFailed": "Tryb został pomyślnie usunięty, ale nie udało się usunąć folderu reguł w {{rulesFolderPath}}. Może być konieczne ręczne usunięcie."
},
"scope": {
"project": "projekt",
"global": "globalny"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Tryb został pomyślnie usunięty, ale nie udało się usunąć folderu reguł w {{rulesFolderPath}}. Może być konieczne ręczne usunięcie."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Roo Code Cloud. Zaloguj się, aby kontynuować.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Falha ao excluir modo personalizado: {{error}}",
"resetFailed": "Falha ao redefinir modos personalizados: {{error}}",
"modeNotFound": "Erro de escrita: Modo não encontrado",
"noWorkspaceForProject": "Nenhuma pasta de workspace encontrada para modo específico do projeto"
"noWorkspaceForProject": "Nenhuma pasta de workspace encontrada para modo específico do projeto",
"rulesCleanupFailed": "O modo foi excluído com sucesso, mas falhou ao excluir a pasta de regras em {{rulesFolderPath}}. Você pode precisar excluí-la manualmente."
},
"scope": {
"project": "projeto",
"global": "global"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "O modo foi removido com sucesso, mas falhou ao excluir a pasta de regras em {{rulesFolderPath}}. Você pode precisar excluí-la manualmente."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Sua organização requer autenticação do Roo Code Cloud. Faça login para continuar.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Не удалось удалить пользовательский режим: {{error}}",
"resetFailed": "Не удалось сбросить пользовательские режимы: {{error}}",
"modeNotFound": "Ошибка записи: Режим не найден",
"noWorkspaceForProject": "Не найдена папка рабочего пространства для режима, специфичного для проекта"
"noWorkspaceForProject": "Не найдена папка рабочего пространства для режима, специфичного для проекта",
"rulesCleanupFailed": "Режим успешно удален, но не удалось удалить папку правил в {{rulesFolderPath}}. Возможно, вам придется удалить ее вручную."
},
"scope": {
"project": "проект",
"global": "глобальный"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Режим успешно удален, но не удалось удалить папку правил в {{rulesFolderPath}}. Возможно, вам придется удалить ее вручную."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Özel mod silme başarısız: {{error}}",
"resetFailed": "Özel modları sıfırlama başarısız: {{error}}",
"modeNotFound": "Yazma hatası: Mod bulunamadı",
"noWorkspaceForProject": "Proje özel modu için çalışma alanı klasörü bulunamadı"
"noWorkspaceForProject": "Proje özel modu için çalışma alanı klasörü bulunamadı",
"rulesCleanupFailed": "Mod başarıyla silindi, ancak {{rulesFolderPath}} konumundaki kurallar klasörü silinemedi. Manuel olarak silmeniz gerekebilir."
},
"scope": {
"project": "proje",
"global": "küresel"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Mod başarıyla kaldırıldı, ancak {{rulesFolderPath}} konumundaki kurallar klasörü silinemedi. Manuel olarak silmeniz gerekebilir."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Kuruluşunuz Roo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.",

View file

@ -147,13 +147,19 @@
"deleteFailed": "Xóa chế độ tùy chỉnh thất bại: {{error}}",
"resetFailed": "Đặt lại chế độ tùy chỉnh thất bại: {{error}}",
"modeNotFound": "Lỗi ghi: Không tìm thấy chế độ",
"noWorkspaceForProject": "Không tìm thấy thư mục workspace cho chế độ dành riêng cho dự án"
"noWorkspaceForProject": "Không tìm thấy thư mục workspace cho chế độ dành riêng cho dự án",
"rulesCleanupFailed": "Đã xóa chế độ thành công, nhưng không thể xóa thư mục quy tắc tại {{rulesFolderPath}}. Bạn có thể cần xóa thủ công."
},
"scope": {
"project": "dự án",
"global": "toàn cầu"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "Đã xóa chế độ thành công, nhưng không thể xóa thư mục quy tắc tại {{rulesFolderPath}}. Bạn có thể cần xóa thủ công."
}
},
"mdm": {
"errors": {
"cloud_auth_required": "Tổ chức của bạn yêu cầu xác thực Roo Code Cloud. Vui lòng đăng nhập để tiếp tục.",

View file

@ -152,13 +152,19 @@
"deleteFailed": "删除自定义模式失败:{{error}}",
"resetFailed": "重置自定义模式失败:{{error}}",
"modeNotFound": "写入错误:未找到模式",
"noWorkspaceForProject": "未找到项目特定模式的工作区文件夹"
"noWorkspaceForProject": "未找到项目特定模式的工作区文件夹",
"rulesCleanupFailed": "模式删除成功,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。"
},
"scope": {
"project": "项目",
"global": "全局"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "模式已成功移除,但无法删除位于 {{rulesFolderPath}} 的规则文件夹。您可能需要手动删除。"
}
},
"mdm": {
"errors": {
"cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。",

View file

@ -147,13 +147,19 @@
"deleteFailed": "刪除自訂模式失敗:{{error}}",
"resetFailed": "重設自訂模式失敗:{{error}}",
"modeNotFound": "寫入錯誤:未找到模式",
"noWorkspaceForProject": "未找到專案特定模式的工作區資料夾"
"noWorkspaceForProject": "未找到專案特定模式的工作區資料夾",
"rulesCleanupFailed": "模式已成功刪除,但無法刪除位於 {{rulesFolderPath}} 的規則資料夾。您可能需要手動刪除。"
},
"scope": {
"project": "專案",
"global": "全域"
}
},
"marketplace": {
"mode": {
"rulesCleanupFailed": "模式已成功移除,但無法刪除位於 {{rulesFolderPath}} 的規則資料夾。您可能需要手動刪除。"
}
},
"mdm": {
"errors": {
"cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。",

View file

@ -9,14 +9,18 @@ import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
import { t } from "../../i18n"
import { TelemetryService } from "@roo-code/telemetry"
import type { CustomModesManager } from "../../core/config/CustomModesManager"
export class MarketplaceManager {
private configLoader: RemoteConfigLoader
private installer: SimpleInstaller
constructor(private readonly context: vscode.ExtensionContext) {
constructor(
private readonly context: vscode.ExtensionContext,
private readonly customModesManager?: CustomModesManager,
) {
this.configLoader = new RemoteConfigLoader()
this.installer = new SimpleInstaller(context)
this.installer = new SimpleInstaller(context, customModesManager)
}
async getMarketplaceItems(): Promise<{ items: MarketplaceItem[]; errors?: string[] }> {

View file

@ -5,6 +5,7 @@ import * as yaml from "yaml"
import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types"
import { GlobalFileNames } from "../../shared/globalFileNames"
import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
import type { CustomModesManager } from "../../core/config/CustomModesManager"
export interface InstallOptions extends InstallMarketplaceItemOptions {
target: "project" | "global"
@ -12,7 +13,10 @@ export interface InstallOptions extends InstallMarketplaceItemOptions {
}
export class SimpleInstaller {
constructor(private readonly context: vscode.ExtensionContext) {}
constructor(
private readonly context: vscode.ExtensionContext,
private readonly customModesManager?: CustomModesManager,
) {}
async installItem(item: MarketplaceItem, options: InstallOptions): Promise<{ filePath: string; line?: number }> {
const { target } = options
@ -40,6 +44,48 @@ export class SimpleInstaller {
throw new Error("Mode content should not be an array")
}
// If CustomModesManager is available, use importModeWithRules
if (this.customModesManager) {
// Transform marketplace content to import format (wrap in customModes array)
const importData = {
customModes: [yaml.parse(item.content)],
}
const importYaml = yaml.stringify(importData)
// Call customModesManager.importModeWithRules
const result = await this.customModesManager.importModeWithRules(importYaml, target)
if (!result.success) {
throw new Error(result.error || "Failed to import mode")
}
// Return the file path and line number for VS Code to open
const filePath = await this.getModeFilePath(target)
// Try to find the line number where the mode was added
let line: number | undefined
try {
const fileContent = await fs.readFile(filePath, "utf-8")
const lines = fileContent.split("\n")
const modeData = yaml.parse(item.content)
// Find the line containing the slug of the added mode
if (modeData?.slug) {
const slugLineIndex = lines.findIndex(
(l) => l.includes(`slug: ${modeData.slug}`) || l.includes(`slug: "${modeData.slug}"`),
)
if (slugLineIndex >= 0) {
line = slugLineIndex + 1 // Convert to 1-based line number
}
}
} catch (error) {
// If we can't find the line number, that's okay
}
return { filePath, line }
}
// Fallback to original implementation if CustomModesManager is not available
const filePath = await this.getModeFilePath(target)
const modeData = yaml.parse(item.content)
@ -248,56 +294,38 @@ export class SimpleInstaller {
}
private async removeMode(item: MarketplaceItem, target: "project" | "global"): Promise<void> {
const filePath = await this.getModeFilePath(target)
try {
const existing = await fs.readFile(filePath, "utf-8")
let existingData: any
try {
const parsed = yaml.parse(existing)
// Ensure we have a valid object
existingData = parsed && typeof parsed === "object" ? parsed : {}
} catch (parseError) {
// If we can't parse the file, we can't safely remove a mode
const fileName = target === "project" ? ".roomodes" : "custom-modes.yaml"
throw new Error(
`Cannot remove mode: The ${fileName} file contains invalid YAML. ` +
`Please fix the syntax errors before removing modes.`,
)
}
// Ensure customModes array exists
if (!existingData.customModes) {
existingData.customModes = []
}
// Parse the item content to get the slug
let content: string
if (Array.isArray(item.content)) {
// Array of McpInstallationMethod objects - use first method
content = item.content[0].content
} else {
content = item.content
}
const modeData = yaml.parse(content || "")
if (!modeData.slug) {
return // Nothing to remove if no slug
}
// Remove mode with matching slug
existingData.customModes = existingData.customModes.filter((mode: any) => mode.slug !== modeData.slug)
// Always write back the file, even if empty
await fs.writeFile(filePath, yaml.stringify(existingData, { lineWidth: 0 }), "utf-8")
} catch (error: any) {
if (error.code === "ENOENT") {
// File doesn't exist, nothing to remove
return
}
throw error
if (!this.customModesManager) {
throw new Error("CustomModesManager is not available")
}
// Parse the item content to get the slug
let content: string
if (Array.isArray(item.content)) {
// Array of McpInstallationMethod objects - use first method
content = item.content[0].content
} else {
content = item.content || ""
}
let modeSlug: string
try {
const modeData = yaml.parse(content)
modeSlug = modeData.slug
} catch (error) {
throw new Error("Invalid mode content: unable to parse YAML")
}
if (!modeSlug) {
throw new Error("Mode missing slug identifier")
}
// Get the current modes to determine the source
const modes = await this.customModesManager.getCustomModes()
const mode = modes.find((m) => m.slug === modeSlug)
// Use CustomModesManager to delete the mode configuration
// This also handles rules folder deletion
await this.customModesManager.deleteCustomMode(modeSlug, true)
}
private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise<void> {

View file

@ -4,10 +4,19 @@ import { SimpleInstaller } from "../SimpleInstaller"
import * as fs from "fs/promises"
import * as yaml from "yaml"
import * as vscode from "vscode"
import * as os from "os"
import type { MarketplaceItem } from "@roo-code/types"
import type { CustomModesManager } from "../../../core/config/CustomModesManager"
import * as path from "path"
import { fileExistsAtPath } from "../../../utils/fs"
vi.mock("fs/promises")
vi.mock("fs/promises", () => ({
readFile: vi.fn(),
writeFile: vi.fn(),
mkdir: vi.fn(),
rm: vi.fn(),
}))
vi.mock("os")
vi.mock("vscode", () => ({
workspace: {
workspaceFolders: [
@ -20,20 +29,33 @@ vi.mock("vscode", () => ({
},
}))
vi.mock("../../../utils/globalContext")
vi.mock("../../../utils/fs")
const mockFs = fs as any
const mockFs = vi.mocked(fs)
describe("SimpleInstaller", () => {
let installer: SimpleInstaller
let mockContext: vscode.ExtensionContext
let mockCustomModesManager: CustomModesManager
beforeEach(() => {
mockContext = {} as vscode.ExtensionContext
installer = new SimpleInstaller(mockContext)
mockCustomModesManager = {
deleteCustomMode: vi.fn().mockResolvedValue(undefined),
importModeWithRules: vi.fn().mockResolvedValue({ success: true }),
getCustomModes: vi.fn().mockResolvedValue([]),
} as any
installer = new SimpleInstaller(mockContext, mockCustomModesManager)
vi.clearAllMocks()
// Mock mkdir to always succeed
mockFs.mkdir.mockResolvedValue(undefined as any)
// Mock rm to always succeed
mockFs.rm.mockResolvedValue(undefined as any)
// Mock os.homedir
vi.mocked(os.homedir).mockReturnValue("/home/user")
// Mock fileExistsAtPath to return false by default
vi.mocked(fileExistsAtPath).mockResolvedValue(false)
})
describe("installMode", () => {
@ -50,128 +72,69 @@ describe("SimpleInstaller", () => {
}),
}
it("should install mode when .roomodes file does not exist", async () => {
// Mock file not found error
it("should install mode using CustomModesManager", async () => {
// Mock file not found error for getModeFilePath
const notFoundError = new Error("File not found") as any
notFoundError.code = "ENOENT"
mockFs.readFile.mockRejectedValueOnce(notFoundError)
const result = await installer.installItem(mockModeItem, { target: "project" })
expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes"))
expect(mockCustomModesManager.importModeWithRules).toHaveBeenCalled()
// Verify the import was called with correct YAML structure
const importCall = (mockCustomModesManager.importModeWithRules as any).mock.calls[0]
const importedYaml = importCall[0]
const importedData = yaml.parse(importedYaml)
expect(importedData.customModes).toHaveLength(1)
expect(importedData.customModes[0].slug).toBe("test")
})
it("should handle import failure from CustomModesManager", async () => {
mockCustomModesManager.importModeWithRules = vi.fn().mockResolvedValue({
success: false,
error: "Import failed",
})
await expect(installer.installItem(mockModeItem, { target: "project" })).rejects.toThrow("Import failed")
})
it("should throw error for array content in mode", async () => {
const arrayContentMode: MarketplaceItem = {
...mockModeItem,
content: ["content1", "content2"] as any,
}
await expect(installer.installItem(arrayContentMode, { target: "project" })).rejects.toThrow(
"Mode content should not be an array",
)
})
it("should throw error for missing content", async () => {
const noContentMode: MarketplaceItem = {
...mockModeItem,
content: undefined as any,
}
await expect(installer.installItem(noContentMode, { target: "project" })).rejects.toThrow(
"Mode item missing content",
)
})
it("should work without CustomModesManager (fallback)", async () => {
const installerWithoutManager = new SimpleInstaller(mockContext)
// Mock file not found
const notFoundError = new Error("File not found") as any
notFoundError.code = "ENOENT"
mockFs.readFile.mockRejectedValueOnce(notFoundError)
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
const result = await installer.installItem(mockModeItem, { target: "project" })
const result = await installerWithoutManager.installItem(mockModeItem, { target: "project" })
expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes"))
expect(mockFs.writeFile).toHaveBeenCalled()
// Verify the written content contains the new mode
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
expect(writtenData.customModes).toHaveLength(1)
expect(writtenData.customModes[0].slug).toBe("test")
})
it("should install mode when .roomodes contains valid YAML", async () => {
const existingContent = yaml.stringify({
customModes: [{ slug: "existing", name: "Existing Mode", roleDefinition: "Existing", groups: [] }],
})
mockFs.readFile.mockResolvedValueOnce(existingContent)
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
await installer.installItem(mockModeItem, { target: "project" })
expect(mockFs.writeFile).toHaveBeenCalled()
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
// Should contain both existing and new mode
expect(writtenData.customModes).toHaveLength(2)
expect(writtenData.customModes.find((m: any) => m.slug === "existing")).toBeDefined()
expect(writtenData.customModes.find((m: any) => m.slug === "test")).toBeDefined()
})
it("should handle empty .roomodes file", async () => {
// Empty file content
mockFs.readFile.mockResolvedValueOnce("")
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
const result = await installer.installItem(mockModeItem, { target: "project" })
expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes"))
expect(mockFs.writeFile).toHaveBeenCalled()
// Verify the written content contains the new mode
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
expect(writtenData.customModes).toHaveLength(1)
expect(writtenData.customModes[0].slug).toBe("test")
})
it("should handle .roomodes file with null content", async () => {
// File exists but yaml.parse returns null
mockFs.readFile.mockResolvedValueOnce("---\n")
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
const result = await installer.installItem(mockModeItem, { target: "project" })
expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes"))
expect(mockFs.writeFile).toHaveBeenCalled()
// Verify the written content contains the new mode
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
expect(writtenData.customModes).toHaveLength(1)
expect(writtenData.customModes[0].slug).toBe("test")
})
it("should handle .roomodes file without customModes property", async () => {
// File has valid YAML but no customModes property
const contentWithoutCustomModes = yaml.stringify({ someOtherProperty: "value" })
mockFs.readFile.mockResolvedValueOnce(contentWithoutCustomModes)
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
const result = await installer.installItem(mockModeItem, { target: "project" })
expect(result.filePath).toBe(path.join("/test/workspace", ".roomodes"))
expect(mockFs.writeFile).toHaveBeenCalled()
// Verify the written content contains the new mode and preserves other properties
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
expect(writtenData.customModes).toHaveLength(1)
expect(writtenData.customModes[0].slug).toBe("test")
expect(writtenData.someOtherProperty).toBe("value")
})
it("should throw error when .roomodes contains invalid YAML", async () => {
const invalidYaml = "invalid: yaml: content: {"
mockFs.readFile.mockResolvedValueOnce(invalidYaml)
await expect(installer.installItem(mockModeItem, { target: "project" })).rejects.toThrow(
"Cannot install mode: The .roomodes file contains invalid YAML",
)
// Should NOT write to file
expect(mockFs.writeFile).not.toHaveBeenCalled()
})
it("should replace existing mode with same slug", async () => {
const existingContent = yaml.stringify({
customModes: [{ slug: "test", name: "Old Test Mode", roleDefinition: "Old role", groups: [] }],
})
mockFs.readFile.mockResolvedValueOnce(existingContent)
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
await installer.installItem(mockModeItem, { target: "project" })
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
// Should contain only one mode with updated content
expect(writtenData.customModes).toHaveLength(1)
expect(writtenData.customModes[0].slug).toBe("test")
expect(writtenData.customModes[0].name).toBe("Test Mode") // New name
})
})
@ -254,75 +217,133 @@ describe("SimpleInstaller", () => {
}),
}
it("should throw error when .roomodes contains invalid YAML during removal", async () => {
const invalidYaml = "invalid: yaml: content: {"
it("should use CustomModesManager to delete mode and clean up rules folder", async () => {
// Mock that the mode exists with project source
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
{ slug: "test", name: "Test Mode", source: "project" } as any,
])
mockFs.readFile.mockResolvedValueOnce(invalidYaml)
await installer.removeItem(mockModeItem, { target: "project" })
await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow(
"Cannot remove mode: The .roomodes file contains invalid YAML",
// Should call deleteCustomMode with fromMarketplace flag set to true
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
// The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller
expect(fileExistsAtPath).not.toHaveBeenCalled()
expect(mockFs.rm).not.toHaveBeenCalled()
})
it("should handle global mode removal with rules cleanup", async () => {
// Mock that the mode exists with global source
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
{ slug: "test", name: "Test Mode", source: "global" } as any,
])
await installer.removeItem(mockModeItem, { target: "global" })
// Should call deleteCustomMode with fromMarketplace flag set to true
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
// The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller
expect(fileExistsAtPath).not.toHaveBeenCalled()
expect(mockFs.rm).not.toHaveBeenCalled()
})
it("should handle case when rules folder does not exist", async () => {
// Mock that the mode exists
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
{ slug: "test", name: "Test Mode", source: "project" } as any,
])
await installer.removeItem(mockModeItem, { target: "project" })
// Should call deleteCustomMode with fromMarketplace flag set to true
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
// The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller
expect(fileExistsAtPath).not.toHaveBeenCalled()
expect(mockFs.rm).not.toHaveBeenCalled()
})
it("should throw error if deleteCustomMode fails", async () => {
// Mock that the mode exists
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([
{ slug: "test", name: "Test Mode", source: "project" } as any,
])
// Mock that deleteCustomMode fails
mockCustomModesManager.deleteCustomMode = vi.fn().mockRejectedValueOnce(new Error("Permission denied"))
// Should throw the error from deleteCustomMode
await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow("Permission denied")
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
})
it("should handle mode not found in custom modes list", async () => {
// Mock that the mode doesn't exist in the list
vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([])
await installer.removeItem(mockModeItem, { target: "project" })
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true)
// Should not attempt to delete rules folder
expect(fileExistsAtPath).not.toHaveBeenCalled()
expect(mockFs.rm).not.toHaveBeenCalled()
})
it("should throw error when mode content is invalid YAML", async () => {
const invalidModeItem: MarketplaceItem = {
...mockModeItem,
content: "invalid: yaml: content: {",
}
await expect(installer.removeItem(invalidModeItem, { target: "project" })).rejects.toThrow(
"Invalid mode content: unable to parse YAML",
)
// Should NOT write to file
expect(mockFs.writeFile).not.toHaveBeenCalled()
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
})
it("should do nothing when file does not exist", async () => {
const notFoundError = new Error("File not found") as any
notFoundError.code = "ENOENT"
mockFs.readFile.mockRejectedValueOnce(notFoundError)
it("should throw error when mode has no slug", async () => {
const noSlugModeItem: MarketplaceItem = {
...mockModeItem,
content: yaml.stringify({
name: "Test Mode",
roleDefinition: "Test role",
groups: ["read"],
}),
}
// Should not throw
await installer.removeItem(mockModeItem, { target: "project" })
await expect(installer.removeItem(noSlugModeItem, { target: "project" })).rejects.toThrow(
"Mode missing slug identifier",
)
expect(mockFs.writeFile).not.toHaveBeenCalled()
expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled()
})
it("should handle empty .roomodes file during removal", async () => {
// Empty file content
mockFs.readFile.mockResolvedValueOnce("")
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
it("should handle array content format", async () => {
const arrayContentItem: MarketplaceItem = {
...mockModeItem,
content: [
{
content: yaml.stringify({
slug: "test-array",
name: "Test Array Mode",
roleDefinition: "Test role",
groups: ["read"],
}),
},
] as any,
}
// Should not throw
await installer.removeItem(mockModeItem, { target: "project" })
await installer.removeItem(arrayContentItem, { target: "project" })
// Should write back a valid structure with empty customModes
expect(mockFs.writeFile).toHaveBeenCalled()
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
expect(writtenData.customModes).toEqual([])
expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-array", true)
})
it("should handle .roomodes file with null content during removal", async () => {
// File exists but yaml.parse returns null
mockFs.readFile.mockResolvedValueOnce("---\n")
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
it("should throw error when CustomModesManager is not available", async () => {
const installerWithoutManager = new SimpleInstaller(mockContext)
// Should not throw
await installer.removeItem(mockModeItem, { target: "project" })
// Should write back a valid structure with empty customModes
expect(mockFs.writeFile).toHaveBeenCalled()
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
expect(writtenData.customModes).toEqual([])
})
it("should handle .roomodes file without customModes property during removal", async () => {
// File has valid YAML but no customModes property
const contentWithoutCustomModes = yaml.stringify({ someOtherProperty: "value" })
mockFs.readFile.mockResolvedValueOnce(contentWithoutCustomModes)
mockFs.writeFile.mockResolvedValueOnce(undefined as any)
// Should not throw
await installer.removeItem(mockModeItem, { target: "project" })
// Should write back the file with the same content (no modes to remove)
expect(mockFs.writeFile).toHaveBeenCalled()
const writtenContent = mockFs.writeFile.mock.calls[0][1] as string
const writtenData = yaml.parse(writtenContent)
expect(writtenData.customModes).toEqual([])
expect(writtenData.someOtherProperty).toBe("value")
await expect(installerWithoutManager.removeItem(mockModeItem, { target: "project" })).rejects.toThrow(
"CustomModesManager is not available",
)
})
})
})

View file

@ -102,6 +102,7 @@ export interface ExtensionMessage {
| "indexCleared"
| "codebaseIndexConfig"
| "marketplaceInstallResult"
| "marketplaceRemoveResult"
| "marketplaceData"
| "shareTaskSuccess"
| "codeIndexSettingsSaved"

View file

@ -1,4 +1,4 @@
import React, { useMemo, useState } from "react"
import React, { useMemo, useState, useEffect } from "react"
import { MarketplaceItem, TelemetryEventName } from "@roo-code/types"
import { vscode } from "@/utils/vscode"
import { telemetryClient } from "@/utils/TelemetryClient"
@ -10,6 +10,16 @@ import { Button } from "@/components/ui/button"
import { StandardTooltip } from "@/components/ui"
import { MarketplaceInstallModal } from "./MarketplaceInstallModal"
import { useExtensionState } from "@/context/ExtensionStateContext"
import {
AlertDialog,
AlertDialogAction,
AlertDialogCancel,
AlertDialogContent,
AlertDialogDescription,
AlertDialogFooter,
AlertDialogHeader,
AlertDialogTitle,
} from "@/components/ui"
interface ItemInstalledMetadata {
type: string
@ -29,6 +39,30 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
const { t } = useAppTranslation()
const { cwd } = useExtensionState()
const [showInstallModal, setShowInstallModal] = useState(false)
const [showRemoveConfirm, setShowRemoveConfirm] = useState(false)
const [removeTarget, setRemoveTarget] = useState<"project" | "global">("project")
const [removeError, setRemoveError] = useState<string | null>(null)
// Listen for removal result messages
useEffect(() => {
const handleMessage = (event: MessageEvent) => {
const message = event.data
if (message.type === "marketplaceRemoveResult" && message.slug === item.id) {
if (message.success) {
// Removal succeeded - refresh marketplace data
vscode.postMessage({
type: "fetchMarketplaceData",
})
} else {
// Removal failed - show error message to user
setRemoveError(message.error || t("marketplace:items.unknownError"))
}
}
}
window.addEventListener("message", handleMessage)
return () => window.removeEventListener("message", handleMessage)
}, [item.id, t])
const typeLabel = useMemo(() => {
const labels: Partial<Record<MarketplaceItem["type"], string>> = {
@ -92,16 +126,8 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
onClick={() => {
// Determine which installation to remove (prefer project over global)
const target = isInstalledInProject ? "project" : "global"
vscode.postMessage({
type: "removeInstalledMarketplaceItem",
mpItem: item,
mpInstallOptions: { target },
})
// Request fresh marketplace data to update installed status
vscode.postMessage({
type: "fetchMarketplaceData",
})
setRemoveTarget(target)
setShowRemoveConfirm(true)
}}>
{t("marketplace:items.card.remove")}
</Button>
@ -116,6 +142,13 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
{t("marketplace:items.card.install")}
</Button>
)}
{/* Error message display */}
{removeError && (
<div className="text-vscode-errorForeground text-sm mt-2">
{t("marketplace:items.removeFailed", { error: removeError })}
</div>
)}
</div>
</div>
@ -169,6 +202,49 @@ export const MarketplaceItemCard: React.FC<MarketplaceItemCardProps> = ({ item,
onClose={() => setShowInstallModal(false)}
hasWorkspace={!!cwd}
/>
{/* Remove Confirmation Dialog */}
<AlertDialog open={showRemoveConfirm} onOpenChange={setShowRemoveConfirm}>
<AlertDialogContent>
<AlertDialogHeader>
<AlertDialogTitle>
{item.type === "mode"
? t("marketplace:removeConfirm.mode.title")
: t("marketplace:removeConfirm.mcp.title")}
</AlertDialogTitle>
<AlertDialogDescription>
{item.type === "mode" ? (
<>
{t("marketplace:removeConfirm.mode.message", { modeName: item.name })}
<div className="mt-2 text-sm">
{t("marketplace:removeConfirm.mode.rulesWarning")}
</div>
</>
) : (
t("marketplace:removeConfirm.mcp.message", { mcpName: item.name })
)}
</AlertDialogDescription>
</AlertDialogHeader>
<AlertDialogFooter>
<AlertDialogCancel>{t("marketplace:removeConfirm.cancel")}</AlertDialogCancel>
<AlertDialogAction
onClick={() => {
// Clear any previous error
setRemoveError(null)
vscode.postMessage({
type: "removeInstalledMarketplaceItem",
mpItem: item,
mpInstallOptions: { target: removeTarget },
})
setShowRemoveConfirm(false)
}}>
{t("marketplace:removeConfirm.confirm")}
</AlertDialogAction>
</AlertDialogFooter>
</AlertDialogContent>
</AlertDialog>
</>
)
}

View file

@ -70,8 +70,12 @@
"installed": "Instal·lat",
"removeProjectTooltip": "Eliminar del projecte actual",
"removeGlobalTooltip": "Eliminar de la configuració global",
"actionsMenuLabel": "Més accions"
}
"actionsMenuLabel": "Més accions",
"official": "Oficial",
"verified": "Verificat"
},
"removeFailed": "No s'ha pogut eliminar l'element: {{error}}",
"unknownError": "S'ha produït un error desconegut"
},
"install": {
"title": "Instal·lar {{name}}",
@ -125,6 +129,19 @@
"maxSources": "Màxim de {{max}} fonts permeses"
}
},
"removeConfirm": {
"mode": {
"title": "Eliminar el mode",
"message": "Estàs segur que vols eliminar el mode \"{{modeName}}\"?",
"rulesWarning": "Això també eliminarà qualsevol fitxer de regles associat per a aquest mode."
},
"mcp": {
"title": "Eliminar el servidor MCP",
"message": "Estàs segur que vols eliminar el servidor MCP \"{{mcpName}}\"?"
},
"cancel": "Cancel·lar",
"confirm": "Eliminar"
},
"footer": {
"issueText": "Has trobat un problema amb un element del marketplace o tens suggeriments per a nous elements? <0>Obre una incidència de GitHub</0> per fer-nos-ho saber!"
}

View file

@ -70,8 +70,12 @@
"installed": "Installiert",
"removeProjectTooltip": "Aus aktuellem Projekt entfernen",
"removeGlobalTooltip": "Aus globaler Konfiguration entfernen",
"actionsMenuLabel": "Weitere Aktionen"
}
"actionsMenuLabel": "Weitere Aktionen",
"official": "Offiziell",
"verified": "Verifiziert"
},
"removeFailed": "Element konnte nicht entfernt werden: {{error}}",
"unknownError": "Unbekannter Fehler aufgetreten"
},
"install": {
"title": "{{name}} installieren",
@ -125,6 +129,19 @@
"maxSources": "Maximal {{max}} Quellen erlaubt"
}
},
"removeConfirm": {
"mode": {
"title": "Modus entfernen",
"message": "Bist du sicher, dass du den Modus „{{modeName}}“ entfernen möchtest?",
"rulesWarning": "Dadurch werden auch alle zugehörigen Regeldateien für diesen Modus entfernt."
},
"mcp": {
"title": "MCP-Server entfernen",
"message": "Bist du sicher, dass du den MCP-Server „{{mcpName}}“ entfernen möchtest?"
},
"cancel": "Abbrechen",
"confirm": "Entfernen"
},
"footer": {
"issueText": "Problem mit einem Marketplace-Element gefunden oder Vorschläge für neue? <0>Öffne ein GitHub-Issue</0>, um es uns mitzuteilen!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Remove from current project",
"removeGlobalTooltip": "Remove from global configuration",
"actionsMenuLabel": "More actions"
}
},
"removeFailed": "Failed to remove item: {{error}}",
"unknownError": "Unknown error occurred"
},
"install": {
"title": "Install {{name}}",
@ -125,6 +127,19 @@
"maxSources": "Maximum of {{max}} sources allowed"
}
},
"removeConfirm": {
"mode": {
"title": "Remove Mode",
"message": "Are you sure you want to remove the mode \"{{modeName}}\"?",
"rulesWarning": "This will also remove any associated rules files for this mode."
},
"mcp": {
"title": "Remove MCP Server",
"message": "Are you sure you want to remove the MCP server \"{{mcpName}}\"?"
},
"cancel": "Cancel",
"confirm": "Remove"
},
"footer": {
"issueText": "Found a problem with a marketplace item or have suggestions for new ones? <0>Open a GitHub issue</0> to let us know!"
}

View file

@ -70,8 +70,12 @@
"installed": "Instalado",
"removeProjectTooltip": "Eliminar del proyecto actual",
"removeGlobalTooltip": "Eliminar de la configuración global",
"actionsMenuLabel": "Más acciones"
}
"actionsMenuLabel": "Más acciones",
"official": "Oficial",
"verified": "Verificado"
},
"removeFailed": "No se pudo eliminar el elemento: {{error}}",
"unknownError": "Ocurrió un error desconocido"
},
"install": {
"title": "Instalar {{name}}",
@ -125,6 +129,19 @@
"maxSources": "Máximo de {{max}} fuentes permitidas"
}
},
"removeConfirm": {
"mode": {
"title": "Eliminar modo",
"message": "¿Estás seguro de que quieres eliminar el modo \"{{modeName}}\"?",
"rulesWarning": "Esto también eliminará cualquier archivo de reglas asociado para este modo."
},
"mcp": {
"title": "Eliminar servidor MCP",
"message": "¿Estás seguro de que quieres eliminar el servidor MCP \"{{mcpName}}\"?"
},
"cancel": "Cancelar",
"confirm": "Eliminar"
},
"footer": {
"issueText": "¿Encontraste un problema con un elemento del marketplace o tienes sugerencias para nuevos? ¡<0>Abre un issue en GitHub</0> para hacérnoslo saber!"
}

View file

@ -27,7 +27,7 @@
},
"tags": {
"label": "Filtrer par étiquettes :",
"clear": "Effacer les étiquettes",
"clear": "Cliquez pour supprimer '{{count}}' des filtres",
"placeholder": "Tapez pour rechercher et sélectionner des étiquettes...",
"noResults": "Aucune étiquette correspondante trouvée",
"selected": "Affichage des éléments avec l'une des étiquettes sélectionnées",
@ -70,8 +70,12 @@
"installed": "Installé",
"removeProjectTooltip": "Supprimer du projet actuel",
"removeGlobalTooltip": "Supprimer de la configuration globale",
"actionsMenuLabel": "Plus d'actions"
}
"actionsMenuLabel": "Plus d'actions",
"official": "Officiel",
"verified": "Vérifié"
},
"removeFailed": "Échec de la suppression de l'élément : {{error}}",
"unknownError": "Une erreur inconnue est survenue"
},
"install": {
"title": "Installer {{name}}",
@ -125,6 +129,19 @@
"maxSources": "Maximum de {{max}} sources autorisées"
}
},
"removeConfirm": {
"mode": {
"title": "Supprimer le mode",
"message": "Êtes-vous sûr de vouloir supprimer le mode « {{modeName}} » ?",
"rulesWarning": "Cela supprimera également tous les fichiers de règles associés à ce mode."
},
"mcp": {
"title": "Supprimer le serveur MCP",
"message": "Êtes-vous sûr de vouloir supprimer le serveur MCP « {{mcpName}} » ?"
},
"cancel": "Annuler",
"confirm": "Supprimer"
},
"footer": {
"issueText": "Vous avez trouvé un problème avec un élément du marketplace ou avez des suggestions pour de nouveaux éléments ? <0>Ouvrez une issue GitHub</0> pour nous le faire savoir !"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "वर्तमान प्रोजेक्ट से हटाएं",
"removeGlobalTooltip": "ग्लोबल कॉन्फ़िगरेशन से हटाएं",
"actionsMenuLabel": "अधिक क्रियाएं"
}
},
"removeFailed": "आइटम हटाने में विफल: {{error}}",
"unknownError": "अज्ञात त्रुटि हुई"
},
"install": {
"title": "{{name}} इंस्टॉल करें",
@ -125,6 +127,19 @@
"maxSources": "अधिकतम {{max}} स्रोतों की अनुमति है"
}
},
"removeConfirm": {
"mode": {
"title": "मोड हटाएं",
"message": "क्या आप वाकई मोड \"{{modeName}}\" को हटाना चाहते हैं?",
"rulesWarning": "यह इस मोड के लिए किसी भी संबंधित नियम फ़ाइल को भी हटा देगा।"
},
"mcp": {
"title": "MCP सर्वर हटाएं",
"message": "क्या आप वाकई MCP सर्वर \"{{mcpName}}\" को हटाना चाहते हैं?"
},
"cancel": "रद्द करें",
"confirm": "हटाएं"
},
"footer": {
"issueText": "कोई marketplace आइटम के साथ समस्या है या नए आइटम के लिए सुझाव हैं? <0>GitHub issue खोलें</0> हमें बताने के लिए!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Hapus dari proyek saat ini",
"removeGlobalTooltip": "Hapus dari konfigurasi global",
"actionsMenuLabel": "Aksi lainnya"
}
},
"removeFailed": "Gagal menghapus item: {{error}}",
"unknownError": "Terjadi kesalahan yang tidak diketahui"
},
"install": {
"title": "Instal {{name}}",
@ -125,6 +127,19 @@
"maxSources": "Maksimal {{max}} sumber diizinkan"
}
},
"removeConfirm": {
"mode": {
"title": "Hapus Mode",
"message": "Apakah Anda yakin ingin menghapus mode \"{{modeName}}\"?",
"rulesWarning": "Ini juga akan menghapus semua file aturan terkait untuk mode ini."
},
"mcp": {
"title": "Hapus Server MCP",
"message": "Apakah Anda yakin ingin menghapus server MCP \"{{mcpName}}\"?"
},
"cancel": "Batal",
"confirm": "Hapus"
},
"footer": {
"issueText": "Menemukan masalah dengan item marketplace atau punya saran untuk yang baru? <0>Buka GitHub issue</0> untuk memberi tahu kami!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Rimuovi dal progetto corrente",
"removeGlobalTooltip": "Rimuovi dalla configurazione globale",
"actionsMenuLabel": "Altre azioni"
}
},
"removeFailed": "Impossibile rimuovere l'elemento: {{error}}",
"unknownError": "Si è verificato un errore sconosciuto"
},
"install": {
"title": "Installa {{name}}",
@ -125,6 +127,19 @@
"maxSources": "Massimo {{max}} fonti consentite"
}
},
"removeConfirm": {
"mode": {
"title": "Rimuovi modalità",
"message": "Sei sicuro di voler rimuovere la modalità \"{{modeName}}\"?",
"rulesWarning": "Questo rimuoverà anche eventuali file di regole associati per questa modalità."
},
"mcp": {
"title": "Rimuovi server MCP",
"message": "Sei sicuro di voler rimuovere il server MCP \"{{mcpName}}\"?"
},
"cancel": "Annulla",
"confirm": "Rimuovi"
},
"footer": {
"issueText": "Hai trovato un problema con un elemento del marketplace o hai suggerimenti per nuovi elementi? <0>Apri un issue GitHub</0> per farcelo sapere!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "現在のプロジェクトから削除",
"removeGlobalTooltip": "グローバル設定から削除",
"actionsMenuLabel": "その他のアクション"
}
},
"removeFailed": "アイテムの削除に失敗しました: {{error}}",
"unknownError": "不明なエラーが発生しました"
},
"install": {
"title": "{{name}}をインストール",
@ -125,6 +127,19 @@
"maxSources": "最大{{max}}個のソースが許可されています"
}
},
"removeConfirm": {
"mode": {
"title": "モードを削除",
"message": "モード「{{modeName}}」を本当に削除しますか?",
"rulesWarning": "これにより、このモードに関連するルールファイルも削除されます。"
},
"mcp": {
"title": "MCPサーバーを削除",
"message": "MCPサーバー「{{mcpName}}」を本当に削除しますか?"
},
"cancel": "キャンセル",
"confirm": "削除"
},
"footer": {
"issueText": "Marketplaceアイテムで問題を見つけた、または新しいアイテムの提案がありますか<0>GitHub issueを開いて</0>お知らせください!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "현재 프로젝트에서 삭제",
"removeGlobalTooltip": "전역 설정에서 삭제",
"actionsMenuLabel": "추가 작업"
}
},
"removeFailed": "항목을 제거하지 못했습니다: {{error}}",
"unknownError": "알 수 없는 오류가 발생했습니다"
},
"install": {
"title": "{{name}} 설치",
@ -125,6 +127,19 @@
"maxSources": "최대 {{max}}개의 소스가 허용됩니다"
}
},
"removeConfirm": {
"mode": {
"title": "모드 제거",
"message": "정말로 '{{modeName}}' 모드를 제거하시겠습니까?",
"rulesWarning": "이렇게 하면 이 모드와 관련된 모든 규칙 파일도 제거됩니다."
},
"mcp": {
"title": "MCP 서버 제거",
"message": "정말로 '{{mcpName}}' MCP 서버를 제거하시겠습니까?"
},
"cancel": "취소",
"confirm": "제거"
},
"footer": {
"issueText": "Marketplace 아이템에 문제가 있거나 새로운 아이템에 대한 제안이 있나요? <0>GitHub issue를 열어서</0> 알려주세요!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Verwijderen uit huidig project",
"removeGlobalTooltip": "Verwijderen uit globale configuratie",
"actionsMenuLabel": "Meer acties"
}
},
"removeFailed": "Item verwijderen mislukt: {{error}}",
"unknownError": "Onbekende fout opgetreden"
},
"install": {
"title": "{{name}} installeren",
@ -125,6 +127,19 @@
"maxSources": "Maximaal {{max}} bronnen toegestaan"
}
},
"removeConfirm": {
"mode": {
"title": "Modus verwijderen",
"message": "Weet je zeker dat je de modus \"{{modeName}}\" wilt verwijderen?",
"rulesWarning": "Dit verwijdert ook alle bijbehorende regelbestanden voor deze modus."
},
"mcp": {
"title": "MCP-server verwijderen",
"message": "Weet je zeker dat je de MCP-server \"{{mcpName}}\" wilt verwijderen?"
},
"cancel": "Annuleren",
"confirm": "Verwijderen"
},
"footer": {
"issueText": "Heb je een probleem gevonden met een marketplace-item of heb je suggesties voor nieuwe items? <0>Open een GitHub issue</0> om het ons te laten weten!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Usuń z bieżącego projektu",
"removeGlobalTooltip": "Usuń z konfiguracji globalnej",
"actionsMenuLabel": "Więcej akcji"
}
},
"removeFailed": "Nie udało się usunąć elementu: {{error}}",
"unknownError": "Wystąpił nieznany błąd"
},
"install": {
"title": "Zainstaluj {{name}}",
@ -125,6 +127,19 @@
"maxSources": "Maksymalnie {{max}} źródeł dozwolonych"
}
},
"removeConfirm": {
"mode": {
"title": "Usuń tryb",
"message": "Czy na pewno chcesz usunąć tryb „{{modeName}}”?",
"rulesWarning": "Spowoduje to również usunięcie wszelkich powiązanych plików reguł dla tego trybu."
},
"mcp": {
"title": "Usuń serwer MCP",
"message": "Czy na pewno chcesz usunąć serwer MCP „{{mcpName}}”?"
},
"cancel": "Anuluj",
"confirm": "Usuń"
},
"footer": {
"issueText": "Znalazłeś problem z elementem marketplace lub masz sugestie dotyczące nowych elementów? <0>Otwórz issue na GitHub</0>, aby nam o tym powiedzieć!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Remover do projeto atual",
"removeGlobalTooltip": "Remover da configuração global",
"actionsMenuLabel": "Mais ações"
}
},
"removeFailed": "Falha ao remover o item: {{error}}",
"unknownError": "Ocorreu um erro desconhecido"
},
"install": {
"title": "Instalar {{name}}",
@ -125,6 +127,19 @@
"maxSources": "Máximo de {{max}} fontes permitidas"
}
},
"removeConfirm": {
"mode": {
"title": "Remover Modo",
"message": "Tem certeza de que deseja remover o modo \"{{modeName}}\"?",
"rulesWarning": "Isso também removerá todos os arquivos de regras associados a este modo."
},
"mcp": {
"title": "Remover Servidor MCP",
"message": "Tem certeza de que deseja remover o servidor MCP \"{{mcpName}}\"?"
},
"cancel": "Cancelar",
"confirm": "Remover"
},
"footer": {
"issueText": "Encontrou um problema com um item do marketplace ou tem sugestões para novos itens? <0>Abra um issue no GitHub</0> para nos avisar!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Удалить из текущего проекта",
"removeGlobalTooltip": "Удалить из глобальной конфигурации",
"actionsMenuLabel": "Дополнительные действия"
}
},
"removeFailed": "Не удалось удалить элемент: {{error}}",
"unknownError": "Произошла неизвестная ошибка"
},
"install": {
"title": "Установить {{name}}",
@ -125,6 +127,19 @@
"maxSources": "Максимум {{max}} источников разрешено"
}
},
"removeConfirm": {
"mode": {
"title": "Удалить режим",
"message": "Вы уверены, что хотите удалить режим «{{modeName}}»?",
"rulesWarning": "Это также удалит все связанные файлы правил для этого режима."
},
"mcp": {
"title": "Удалить сервер MCP",
"message": "Вы уверены, что хотите удалить сервер MCP «{{mcpName}}»?"
},
"cancel": "Отмена",
"confirm": "Удалить"
},
"footer": {
"issueText": "Нашли проблему с элементом marketplace или есть предложения для новых элементов? <0>Откройте issue на GitHub</0>, чтобы сообщить нам!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Mevcut projeden kaldır",
"removeGlobalTooltip": "Global yapılandırmadan kaldır",
"actionsMenuLabel": "Daha fazla eylem"
}
},
"removeFailed": "Öğe kaldırılamadı: {{error}}",
"unknownError": "Bilinmeyen bir hata oluştu"
},
"install": {
"title": "{{name}} Yükle",
@ -125,6 +127,19 @@
"maxSources": "Maksimum {{max}} kaynak izin verilir"
}
},
"removeConfirm": {
"mode": {
"title": "Modu Kaldır",
"message": "\"{{modeName}}\" modunu kaldırmak istediğinizden emin misiniz?",
"rulesWarning": "Bu, bu mod için ilişkili tüm kural dosyalarını da kaldıracaktır."
},
"mcp": {
"title": "MCP Sunucusunu Kaldır",
"message": "\"{{mcpName}}\" MCP sunucusunu kaldırmak istediğinizden emin misiniz?"
},
"cancel": "İptal",
"confirm": "Kaldır"
},
"footer": {
"issueText": "Bir marketplace öğesi ile ilgili sorun bulduğun veya yeni öğeler için önerilerin var mı? <0>GitHub'da issue aç</0> ve bize bildir!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "Xóa khỏi dự án hiện tại",
"removeGlobalTooltip": "Xóa khỏi cấu hình toàn cục",
"actionsMenuLabel": "Thêm hành động"
}
},
"removeFailed": "Không thể xóa mục: {{error}}",
"unknownError": "Đã xảy ra lỗi không xác định"
},
"install": {
"title": "Cài đặt {{name}}",
@ -125,6 +127,19 @@
"maxSources": "Tối đa {{max}} nguồn được phép"
}
},
"removeConfirm": {
"mode": {
"title": "Xóa chế độ",
"message": "Bạn có chắc chắn muốn xóa chế độ \"{{modeName}}\" không?",
"rulesWarning": "Thao tác này cũng sẽ xóa mọi tệp quy tắc được liên kết cho chế độ này."
},
"mcp": {
"title": "Xóa máy chủ MCP",
"message": "Bạn có chắc chắn muốn xóa máy chủ MCP \"{{mcpName}}\" không?"
},
"cancel": "Hủy",
"confirm": "Xóa"
},
"footer": {
"issueText": "Bạn tìm thấy vấn đề với mục marketplace hoặc có đề xuất cho mục mới? <0>Mở issue GitHub</0> để cho chúng tôi biết!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "从当前项目中移除",
"removeGlobalTooltip": "从全局配置中移除",
"actionsMenuLabel": "更多操作"
}
},
"removeFailed": "删除失败: {{error}}",
"unknownError": "发生未知错误"
},
"install": {
"title": "安装 {{name}}",
@ -125,6 +127,19 @@
"maxSources": "最多允许 {{max}} 个源"
}
},
"removeConfirm": {
"mode": {
"title": "删除模式",
"message": "您确定要删除\"{{modeName}}\"模式吗?",
"rulesWarning": "这也将删除此模式的任何关联规则文件。"
},
"mcp": {
"title": "删除 MCP 服务器",
"message": "您确定要删除 MCP 服务器\"{{mcpName}}\"吗?"
},
"cancel": "取消",
"confirm": "删除"
},
"footer": {
"issueText": "发现 marketplace 项目问题或有新项目建议?<0>在 GitHub 开启 issue</0> 告诉我们!"
}

View file

@ -71,7 +71,9 @@
"removeProjectTooltip": "從目前專案中移除",
"removeGlobalTooltip": "從全域設定中移除",
"actionsMenuLabel": "更多動作"
}
},
"removeFailed": "移除物品失敗: {{error}}",
"unknownError": "發生未知錯誤"
},
"install": {
"title": "安裝 {{name}}",
@ -125,6 +127,19 @@
"maxSources": "最多允許 {{max}} 個來源"
}
},
"removeConfirm": {
"mode": {
"title": "移除模式",
"message": "您確定要移除「{{modeName}}」模式嗎?",
"rulesWarning": "這也會移除此模式的任何相關規則檔案。"
},
"mcp": {
"title": "移除 MCP 伺服器",
"message": "您確定要移除 MCP 伺服器「{{mcpName}}」嗎?"
},
"cancel": "取消",
"confirm": "移除"
},
"footer": {
"issueText": "發現 marketplace 項目問題或有新項目建議?<0>在 GitHub 開啟 issue</0> 告訴我們!"
}