From a76bc0b5ae7e39e99194fd02bb3b197fbb52ae93 Mon Sep 17 00:00:00 2001 From: matt-rudolph Date: Fri, 19 Sep 2025 11:51:28 -0600 Subject: [PATCH 1/6] fix(marketplace): distinguish marketplace-installed modes from local custom modes by origin flags; key installed metadata by marketplaceItemId; restrict removal to marketplace-installed mode in selected scope; add scoped delete in CustomModesManager to avoid unintended deletions (fixes #4828) --- packages/types/src/mode.ts | 3 ++ src/core/config/CustomModesManager.ts | 52 +++++++++++++++++++ .../marketplace/MarketplaceManager.ts | 14 +++-- src/services/marketplace/SimpleInstaller.ts | 44 ++++++++++++---- .../__tests__/SimpleInstaller.spec.ts | 24 +++++---- 5 files changed, 114 insertions(+), 23 deletions(-) diff --git a/packages/types/src/mode.ts b/packages/types/src/mode.ts index 88dcbb9574..0113e271f6 100644 --- a/packages/types/src/mode.ts +++ b/packages/types/src/mode.ts @@ -70,6 +70,9 @@ export const modeConfigSchema = z.object({ customInstructions: z.string().optional(), groups: groupEntryArraySchema, source: z.enum(["global", "project"]).optional(), + // Marketplace origin metadata (optional) + installedFromMarketplace: z.boolean().optional(), + marketplaceItemId: z.string().optional(), }) export type ModeConfig = z.infer diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index a9a2e6a6b5..6a0b4113b5 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -550,6 +550,58 @@ export class CustomModesManager { vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage })) } } + /** + * Deletes a custom mode only from the specified source (project or global) + * without affecting the other scope. Also handles rules folder cleanup. + * @param slug - The mode slug + * @param source - "project" or "global" + * @param fromMarketplace - Whether this deletion was initiated by marketplace flows + */ + public async deleteCustomModeForSource( + slug: string, + source: "project" | "global", + fromMarketplace = false, + ): Promise { + try { + const settingsPath = await this.getCustomModesFilePath() + const roomodesPath = await this.getWorkspaceRoomodes() + + let targetPath: string | undefined + let modeToDelete: ModeConfig | undefined + + if (source === "project") { + if (!roomodesPath) { + throw new Error(t("common:customModes.errors.noWorkspaceForProject")) + } + targetPath = roomodesPath + const roomodesModes = await this.loadModesFromFile(roomodesPath) + modeToDelete = roomodesModes.find((m) => m.slug === slug) + } else { + targetPath = settingsPath + const settingsModes = await this.loadModesFromFile(settingsPath) + modeToDelete = settingsModes.find((m) => m.slug === slug) + } + + if (!modeToDelete) { + throw new Error(t("common:customModes.errors.modeNotFound")) + } + + await this.queueWrite(async () => { + // Delete only from the selected source file + await this.updateModesInFile(targetPath!, (modes) => modes.filter((m) => m.slug !== slug)) + + // Delete associated rules folder using the located mode (preserves correct scope) + await this.deleteRulesFolder(slug, modeToDelete!, fromMarketplace) + + // Refresh state and clear caches + this.clearCache() + await this.refreshMergedState() + }) + } catch (error) { + const errorMessage = error instanceof Error ? error.message : String(error) + vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage })) + } + } /** * Deletes the rules folder for a specific mode diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index dfde5600b9..474a53ac36 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -258,8 +258,11 @@ export class MarketplaceManager { const data = yaml.parse(content) if (data?.customModes && Array.isArray(data.customModes)) { for (const mode of data.customModes) { - if (mode.slug) { - metadata[mode.slug] = { + // Only consider marketplace-installed modes and key by marketplaceItemId + const fromMarketplace = (mode as any)?.installedFromMarketplace === true + const marketplaceItemId = (mode as any)?.marketplaceItemId + if (fromMarketplace && typeof marketplaceItemId === "string" && marketplaceItemId.length > 0) { + metadata[marketplaceItemId] = { type: "mode", } } @@ -303,8 +306,11 @@ export class MarketplaceManager { const data = yaml.parse(content) if (data?.customModes && Array.isArray(data.customModes)) { for (const mode of data.customModes) { - if (mode.slug) { - metadata[mode.slug] = { + // Only consider marketplace-installed modes and key by marketplaceItemId + const fromMarketplace = (mode as any)?.installedFromMarketplace === true + const marketplaceItemId = (mode as any)?.marketplaceItemId + if (fromMarketplace && typeof marketplaceItemId === "string" && marketplaceItemId.length > 0) { + metadata[marketplaceItemId] = { type: "mode", } } diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index be002e2f1d..f951c0054c 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -7,9 +7,10 @@ import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import type { CustomModesManager } from "../../core/config/CustomModesManager" -export interface InstallOptions extends InstallMarketplaceItemOptions { +export interface InstallOptions { target: "project" | "global" selectedIndex?: number // Which installation method to use (for array content) + parameters?: Record } export class SimpleInstaller { @@ -47,8 +48,14 @@ export class SimpleInstaller { // If CustomModesManager is available, use importModeWithRules if (this.customModesManager) { // Transform marketplace content to import format (wrap in customModes array) + const parsedMode = yaml.parse(item.content) + // Annotate marketplace origin to disambiguate from user-created modes + if (parsedMode && typeof parsedMode === "object") { + ;(parsedMode as any).installedFromMarketplace = true + ;(parsedMode as any).marketplaceItemId = item.id + } const importData = { - customModes: [yaml.parse(item.content)], + customModes: [parsedMode], } const importYaml = yaml.stringify(importData) @@ -72,7 +79,7 @@ export class SimpleInstaller { // 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}"`), + (l: string) => l.includes(`slug: ${modeData.slug}`) || l.includes(`slug: "${modeData.slug}"`), ) if (slugLineIndex >= 0) { line = slugLineIndex + 1 // Convert to 1-based line number @@ -88,6 +95,11 @@ export class SimpleInstaller { // Fallback to original implementation if CustomModesManager is not available const filePath = await this.getModeFilePath(target) const modeData = yaml.parse(item.content) + // Annotate marketplace origin fields for fallback path as well + if (modeData && typeof modeData === "object") { + ;(modeData as any).installedFromMarketplace = true + ;(modeData as any).marketplaceItemId = item.id + } // Read existing file or create new structure let existingData: any = { customModes: [] } @@ -143,7 +155,7 @@ export class SimpleInstaller { const addedMode = existingData.customModes[addedModeIndex] if (addedMode?.slug) { const slugLineIndex = lines.findIndex( - (l) => l.includes(`slug: ${addedMode.slug}`) || l.includes(`slug: "${addedMode.slug}"`), + (l: string) => l.includes(`slug: ${addedMode.slug}`) || l.includes(`slug: "${addedMode.slug}"`), ) if (slugLineIndex >= 0) { line = slugLineIndex + 1 // Convert to 1-based line number @@ -319,13 +331,27 @@ export class SimpleInstaller { throw new Error("Mode missing slug identifier") } - // Get the current modes to determine the source + // Get the current modes and locate the exact marketplace-installed mode for the selected target const modes = await this.customModesManager.getCustomModes() - const mode = modes.find((m) => m.slug === modeSlug) + const candidate = modes.find( + (m: any) => + m.slug === modeSlug && + m.installedFromMarketplace === true && + m.marketplaceItemId === item.id && + m.source === target, + ) - // Use CustomModesManager to delete the mode configuration - // This also handles rules folder deletion - await this.customModesManager.deleteCustomMode(modeSlug, true) + if (!candidate) { + throw new Error("This mode was not installed from the marketplace for the selected target") + } + + // Delete only from the selected source to avoid unintended removals + if (typeof (this.customModesManager as any).deleteCustomModeForSource === "function") { + await (this.customModesManager as any).deleteCustomModeForSource(modeSlug, target, true) + } else { + // Fallback to legacy deletion if helper is not available + await this.customModesManager.deleteCustomMode(modeSlug, true) + } } private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise { diff --git a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts index 94684056d4..bd2828b3cc 100644 --- a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts +++ b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts @@ -220,7 +220,7 @@ describe("SimpleInstaller", () => { 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, + { slug: "test", name: "Test Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) await installer.removeItem(mockModeItem, { target: "project" }) @@ -235,7 +235,7 @@ describe("SimpleInstaller", () => { 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, + { slug: "test", name: "Test Mode", source: "global", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) await installer.removeItem(mockModeItem, { target: "global" }) @@ -250,7 +250,7 @@ describe("SimpleInstaller", () => { 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, + { slug: "test", name: "Test Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) await installer.removeItem(mockModeItem, { target: "project" }) @@ -265,7 +265,7 @@ describe("SimpleInstaller", () => { 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, + { slug: "test", name: "Test Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) // Mock that deleteCustomMode fails mockCustomModesManager.deleteCustomMode = vi.fn().mockRejectedValueOnce(new Error("Permission denied")) @@ -276,16 +276,15 @@ describe("SimpleInstaller", () => { expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) }) - it("should handle mode not found in custom modes list", async () => { + it("should throw when mode is not marketplace-installed for selected target", async () => { // Mock that the mode doesn't exist in the list vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([]) - await installer.removeItem(mockModeItem, { target: "project" }) + await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow( + "This mode was not installed from the marketplace for the selected target", + ) - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) - // Should not attempt to delete rules folder - expect(fileExistsAtPath).not.toHaveBeenCalled() - expect(mockFs.rm).not.toHaveBeenCalled() + expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled() }) it("should throw error when mode content is invalid YAML", async () => { @@ -333,6 +332,11 @@ describe("SimpleInstaller", () => { ] as any, } + // Mock installed marketplace mode with matching marketplaceItemId and source + vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([ + { slug: "test-array", name: "Test Array Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, + ]) + await installer.removeItem(arrayContentItem, { target: "project" }) expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-array", true) From c6b644a1b622a5f983bb42c4b171ccee61e35d38 Mon Sep 17 00:00:00 2001 From: matt-rudolph Date: Fri, 19 Sep 2025 14:28:11 -0600 Subject: [PATCH 2/6] refactor(marketplace): typed origin checks + helper; scoped deletion safety and i18n; atomic deleteCustomModeForSource; update tests and en locale --- src/core/config/CustomModesManager.ts | 50 +++++++-------- src/i18n/locales/en/marketplace.json | 3 + .../marketplace/MarketplaceManager.ts | 63 +++++++++++-------- src/services/marketplace/SimpleInstaller.ts | 23 +++---- .../__tests__/SimpleInstaller.spec.ts | 31 ++++----- 5 files changed, 92 insertions(+), 78 deletions(-) diff --git a/src/core/config/CustomModesManager.ts b/src/core/config/CustomModesManager.ts index 6a0b4113b5..08d100ae43 100644 --- a/src/core/config/CustomModesManager.ts +++ b/src/core/config/CustomModesManager.ts @@ -563,35 +563,35 @@ export class CustomModesManager { fromMarketplace = false, ): Promise { try { - const settingsPath = await this.getCustomModesFilePath() - const roomodesPath = await this.getWorkspaceRoomodes() - - let targetPath: string | undefined - let modeToDelete: ModeConfig | undefined - - if (source === "project") { - if (!roomodesPath) { - throw new Error(t("common:customModes.errors.noWorkspaceForProject")) - } - targetPath = roomodesPath - const roomodesModes = await this.loadModesFromFile(roomodesPath) - modeToDelete = roomodesModes.find((m) => m.slug === slug) - } else { - targetPath = settingsPath - const settingsModes = await this.loadModesFromFile(settingsPath) - modeToDelete = settingsModes.find((m) => m.slug === slug) - } - - if (!modeToDelete) { - throw new Error(t("common:customModes.errors.modeNotFound")) - } - await this.queueWrite(async () => { + const settingsPath = await this.getCustomModesFilePath() + const roomodesPath = await this.getWorkspaceRoomodes() + + let targetPath: string + let modeToDelete: ModeConfig | undefined + + if (source === "project") { + if (!roomodesPath) { + throw new Error(t("common:customModes.errors.noWorkspaceForProject")) + } + targetPath = roomodesPath + const roomodesModes = await this.loadModesFromFile(roomodesPath) + modeToDelete = roomodesModes.find((m) => m.slug === slug) + } else { + targetPath = settingsPath + const settingsModes = await this.loadModesFromFile(settingsPath) + modeToDelete = settingsModes.find((m) => m.slug === slug) + } + + if (!modeToDelete) { + throw new Error(t("common:customModes.errors.modeNotFound")) + } + // Delete only from the selected source file - await this.updateModesInFile(targetPath!, (modes) => modes.filter((m) => m.slug !== slug)) + await this.updateModesInFile(targetPath, (modes) => modes.filter((m) => m.slug !== slug)) // Delete associated rules folder using the located mode (preserves correct scope) - await this.deleteRulesFolder(slug, modeToDelete!, fromMarketplace) + await this.deleteRulesFolder(slug, modeToDelete, fromMarketplace) // Refresh state and clear caches this.clearCache() diff --git a/src/i18n/locales/en/marketplace.json b/src/i18n/locales/en/marketplace.json index 17ae20078a..7f473ab801 100644 --- a/src/i18n/locales/en/marketplace.json +++ b/src/i18n/locales/en/marketplace.json @@ -65,5 +65,8 @@ "removing": "Removing item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" removed successfully", "removeError": "Failed to remove \"{{itemName}}\": {{errorMessage}}" + }, + "errors": { + "scopedDeletionNotSupported": "Scoped deletion is not supported in this version. Please update Roo Code to the latest version and try again." } } diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index 474a53ac36..704b930a21 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -4,7 +4,8 @@ import * as path from "path" import * as vscode from "vscode" import * as yaml from "yaml" -import type { OrganizationSettings, MarketplaceItem, MarketplaceItemType, McpMarketplaceItem } from "@roo-code/types" +import type { OrganizationSettings, MarketplaceItem, MarketplaceItemType, McpMarketplaceItem, ModeConfig } from "@roo-code/types" +import { customModesSettingsSchema } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -241,6 +242,38 @@ export class MarketplaceManager { return metadata } + // Helper: identify marketplace-installed modes + private isMarketplaceInstalledMode( + mode: ModeConfig, + ): mode is ModeConfig & { installedFromMarketplace: true; marketplaceItemId: string } { + return ( + mode.installedFromMarketplace === true && + typeof mode.marketplaceItemId === "string" && + mode.marketplaceItemId.length > 0 + ) + } + + // Helper: parse YAML and collect installed mode metadata with proper typing + private collectInstalledModesFromYaml( + content: string, + out: Record, + ): void { + try { + const parsed = yaml.parse(content) + const result = customModesSettingsSchema.safeParse(parsed) + if (!result.success) { + return + } + for (const mode of result.data.customModes) { + if (this.isMarketplaceInstalledMode(mode)) { + out[mode.marketplaceItemId!] = { type: "mode" } + } + } + } catch { + // Ignore parse errors here; caller handles file existence/errors + } + } + /** * Check for project-level installed items */ @@ -255,19 +288,7 @@ export class MarketplaceManager { const projectModesPath = path.join(workspaceFolder.uri.fsPath, ".roomodes") try { const content = await fs.readFile(projectModesPath, "utf-8") - const data = yaml.parse(content) - if (data?.customModes && Array.isArray(data.customModes)) { - for (const mode of data.customModes) { - // Only consider marketplace-installed modes and key by marketplaceItemId - const fromMarketplace = (mode as any)?.installedFromMarketplace === true - const marketplaceItemId = (mode as any)?.marketplaceItemId - if (fromMarketplace && typeof marketplaceItemId === "string" && marketplaceItemId.length > 0) { - metadata[marketplaceItemId] = { - type: "mode", - } - } - } - } + this.collectInstalledModesFromYaml(content, metadata) } catch (error) { // File doesn't exist or can't be read, skip } @@ -303,19 +324,7 @@ export class MarketplaceManager { const globalModesPath = path.join(globalSettingsPath, GlobalFileNames.customModes) try { const content = await fs.readFile(globalModesPath, "utf-8") - const data = yaml.parse(content) - if (data?.customModes && Array.isArray(data.customModes)) { - for (const mode of data.customModes) { - // Only consider marketplace-installed modes and key by marketplaceItemId - const fromMarketplace = (mode as any)?.installedFromMarketplace === true - const marketplaceItemId = (mode as any)?.marketplaceItemId - if (fromMarketplace && typeof marketplaceItemId === "string" && marketplaceItemId.length > 0) { - metadata[marketplaceItemId] = { - type: "mode", - } - } - } - } + this.collectInstalledModesFromYaml(content, metadata) } catch (error) { // File doesn't exist or can't be read, skip } diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index f951c0054c..6c034a1def 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -2,10 +2,11 @@ import * as vscode from "vscode" import * as path from "path" import * as fs from "fs/promises" import * as yaml from "yaml" -import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter } from "@roo-code/types" +import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter, ModeConfig } from "@roo-code/types" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import type { CustomModesManager } from "../../core/config/CustomModesManager" +import { t } from "../../i18n" export interface InstallOptions { target: "project" | "global" @@ -28,7 +29,7 @@ export class SimpleInstaller { case "mcp": return await this.installMcp(item, target, options) default: - throw new Error(`Unsupported item type: ${(item as any).type}`) + throw new Error("Unsupported item type") } } @@ -51,8 +52,8 @@ export class SimpleInstaller { const parsedMode = yaml.parse(item.content) // Annotate marketplace origin to disambiguate from user-created modes if (parsedMode && typeof parsedMode === "object") { - ;(parsedMode as any).installedFromMarketplace = true - ;(parsedMode as any).marketplaceItemId = item.id + ;(parsedMode as ModeConfig).installedFromMarketplace = true + ;(parsedMode as ModeConfig).marketplaceItemId = item.id } const importData = { customModes: [parsedMode], @@ -97,8 +98,8 @@ export class SimpleInstaller { const modeData = yaml.parse(item.content) // Annotate marketplace origin fields for fallback path as well if (modeData && typeof modeData === "object") { - ;(modeData as any).installedFromMarketplace = true - ;(modeData as any).marketplaceItemId = item.id + ;(modeData as ModeConfig).installedFromMarketplace = true + ;(modeData as ModeConfig).marketplaceItemId = item.id } // Read existing file or create new structure @@ -301,7 +302,7 @@ export class SimpleInstaller { await this.removeMcp(item, target) break default: - throw new Error(`Unsupported item type: ${(item as any).type}`) + throw new Error("Unsupported item type") } } @@ -334,7 +335,7 @@ export class SimpleInstaller { // Get the current modes and locate the exact marketplace-installed mode for the selected target const modes = await this.customModesManager.getCustomModes() const candidate = modes.find( - (m: any) => + (m: ModeConfig) => m.slug === modeSlug && m.installedFromMarketplace === true && m.marketplaceItemId === item.id && @@ -342,15 +343,15 @@ export class SimpleInstaller { ) if (!candidate) { - throw new Error("This mode was not installed from the marketplace for the selected target") + throw new Error(t("common:customModes.errors.modeNotFound")) } // Delete only from the selected source to avoid unintended removals if (typeof (this.customModesManager as any).deleteCustomModeForSource === "function") { await (this.customModesManager as any).deleteCustomModeForSource(modeSlug, target, true) } else { - // Fallback to legacy deletion if helper is not available - await this.customModesManager.deleteCustomMode(modeSlug, true) + // Scoped deletion not supported in this version + throw new Error(t("marketplace:errors.scopedDeletionNotSupported")) } } diff --git a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts index bd2828b3cc..bdadbe2a54 100644 --- a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts +++ b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts @@ -42,6 +42,7 @@ describe("SimpleInstaller", () => { mockContext = {} as vscode.ExtensionContext mockCustomModesManager = { deleteCustomMode: vi.fn().mockResolvedValue(undefined), + deleteCustomModeForSource: vi.fn().mockResolvedValue(undefined), importModeWithRules: vi.fn().mockResolvedValue({ success: true }), getCustomModes: vi.fn().mockResolvedValue([]), } as any @@ -225,8 +226,8 @@ describe("SimpleInstaller", () => { await installer.removeItem(mockModeItem, { target: "project" }) - // Should call deleteCustomMode with fromMarketplace flag set to true - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + // Should call scoped deletion for the selected source with fromMarketplace flag set to true + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "project", true) // The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller expect(fileExistsAtPath).not.toHaveBeenCalled() expect(mockFs.rm).not.toHaveBeenCalled() @@ -240,8 +241,8 @@ describe("SimpleInstaller", () => { await installer.removeItem(mockModeItem, { target: "global" }) - // Should call deleteCustomMode with fromMarketplace flag set to true - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + // Should call scoped deletion for the selected source with fromMarketplace flag set to true + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "global", true) // The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller expect(fileExistsAtPath).not.toHaveBeenCalled() expect(mockFs.rm).not.toHaveBeenCalled() @@ -255,8 +256,8 @@ describe("SimpleInstaller", () => { await installer.removeItem(mockModeItem, { target: "project" }) - // Should call deleteCustomMode with fromMarketplace flag set to true - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + // Should call scoped deletion for the selected source with fromMarketplace flag set to true + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "project", true) // The rules folder deletion is now handled by CustomModesManager, not SimpleInstaller expect(fileExistsAtPath).not.toHaveBeenCalled() expect(mockFs.rm).not.toHaveBeenCalled() @@ -267,24 +268,24 @@ describe("SimpleInstaller", () => { vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([ { slug: "test", name: "Test Mode", source: "project", installedFromMarketplace: true, marketplaceItemId: "test-mode" } as any, ]) - // Mock that deleteCustomMode fails - mockCustomModesManager.deleteCustomMode = vi.fn().mockRejectedValueOnce(new Error("Permission denied")) + // Mock that scoped deletion fails + ;(mockCustomModesManager as any).deleteCustomModeForSource = vi + .fn() + .mockRejectedValueOnce(new Error("Permission denied")) - // Should throw the error from deleteCustomMode + // Should throw the error from scoped deletion await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow("Permission denied") - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test", true) + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test", "project", true) }) it("should throw when mode is not marketplace-installed for selected target", async () => { // Mock that the mode doesn't exist in the list vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([]) - await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow( - "This mode was not installed from the marketplace for the selected target", - ) + await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow(/Mode not found/) - expect(mockCustomModesManager.deleteCustomMode).not.toHaveBeenCalled() + expect((mockCustomModesManager as any).deleteCustomModeForSource).not.toHaveBeenCalled() }) it("should throw error when mode content is invalid YAML", async () => { @@ -339,7 +340,7 @@ describe("SimpleInstaller", () => { await installer.removeItem(arrayContentItem, { target: "project" }) - expect(mockCustomModesManager.deleteCustomMode).toHaveBeenCalledWith("test-array", true) + expect((mockCustomModesManager as any).deleteCustomModeForSource).toHaveBeenCalledWith("test-array", "project", true) }) it("should throw error when CustomModesManager is not available", async () => { From 4d3433d74cae0d6b4280b412e863a8e9ddb914e7 Mon Sep 17 00:00:00 2001 From: matt-rudolph Date: Fri, 19 Sep 2025 14:38:54 -0600 Subject: [PATCH 3/6] fix(marketplace): use existing i18n key for missing marketplace-installed mode; remove added en locale key to satisfy translation check --- src/i18n/locales/en/marketplace.json | 3 --- src/services/marketplace/SimpleInstaller.ts | 2 +- 2 files changed, 1 insertion(+), 4 deletions(-) diff --git a/src/i18n/locales/en/marketplace.json b/src/i18n/locales/en/marketplace.json index 7f473ab801..17ae20078a 100644 --- a/src/i18n/locales/en/marketplace.json +++ b/src/i18n/locales/en/marketplace.json @@ -65,8 +65,5 @@ "removing": "Removing item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" removed successfully", "removeError": "Failed to remove \"{{itemName}}\": {{errorMessage}}" - }, - "errors": { - "scopedDeletionNotSupported": "Scoped deletion is not supported in this version. Please update Roo Code to the latest version and try again." } } diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index 6c034a1def..960dcedc0b 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -351,7 +351,7 @@ export class SimpleInstaller { await (this.customModesManager as any).deleteCustomModeForSource(modeSlug, target, true) } else { // Scoped deletion not supported in this version - throw new Error(t("marketplace:errors.scopedDeletionNotSupported")) + throw new Error("Scoped deletion is not supported in this version") } } From d3a24a3c6a1ba4ac5b9dfe32b32f455ee878e762 Mon Sep 17 00:00:00 2001 From: matt-rudolph Date: Fri, 19 Sep 2025 14:59:00 -0600 Subject: [PATCH 4/6] test: relax SimpleInstaller not-installed assertion to accept i18n key string on Ubuntu --- src/services/marketplace/__tests__/SimpleInstaller.spec.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts index bdadbe2a54..061fb81dc9 100644 --- a/src/services/marketplace/__tests__/SimpleInstaller.spec.ts +++ b/src/services/marketplace/__tests__/SimpleInstaller.spec.ts @@ -283,7 +283,7 @@ describe("SimpleInstaller", () => { // Mock that the mode doesn't exist in the list vi.mocked(mockCustomModesManager.getCustomModes).mockResolvedValueOnce([]) - await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow(/Mode not found/) + await expect(installer.removeItem(mockModeItem, { target: "project" })).rejects.toThrow(/(Mode not found|customModes\.errors\.modeNotFound)/) expect((mockCustomModesManager as any).deleteCustomModeForSource).not.toHaveBeenCalled() }) From 4e480496ac05fb891953c6d97e8c2437106f36e1 Mon Sep 17 00:00:00 2001 From: matt-rudolph Date: Tue, 23 Sep 2025 16:10:22 -0600 Subject: [PATCH 5/6] fix(marketplace): correct mode slug handling and update en marketplace i18n Refs #8181 --- src/i18n/locales/en/marketplace.json | 3 ++- src/services/marketplace/MarketplaceManager.ts | 15 +++++++++------ src/services/marketplace/SimpleInstaller.ts | 18 ++++++++---------- 3 files changed, 19 insertions(+), 17 deletions(-) diff --git a/src/i18n/locales/en/marketplace.json b/src/i18n/locales/en/marketplace.json index 17ae20078a..b911cb8ec5 100644 --- a/src/i18n/locales/en/marketplace.json +++ b/src/i18n/locales/en/marketplace.json @@ -64,6 +64,7 @@ "installError": "Failed to install \"{{itemName}}\": {{errorMessage}}", "removing": "Removing item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" removed successfully", - "removeError": "Failed to remove \"{{itemName}}\": {{errorMessage}}" + "removeError": "Failed to remove \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Mode not found" } } diff --git a/src/services/marketplace/MarketplaceManager.ts b/src/services/marketplace/MarketplaceManager.ts index 704b930a21..72ac6b64da 100644 --- a/src/services/marketplace/MarketplaceManager.ts +++ b/src/services/marketplace/MarketplaceManager.ts @@ -4,7 +4,13 @@ import * as path from "path" import * as vscode from "vscode" import * as yaml from "yaml" -import type { OrganizationSettings, MarketplaceItem, MarketplaceItemType, McpMarketplaceItem, ModeConfig } from "@roo-code/types" +import type { + OrganizationSettings, + MarketplaceItem, + MarketplaceItemType, + McpMarketplaceItem, + ModeConfig, +} from "@roo-code/types" import { customModesSettingsSchema } from "@roo-code/types" import { TelemetryService } from "@roo-code/telemetry" import { CloudService } from "@roo-code/cloud" @@ -254,10 +260,7 @@ export class MarketplaceManager { } // Helper: parse YAML and collect installed mode metadata with proper typing - private collectInstalledModesFromYaml( - content: string, - out: Record, - ): void { + private collectInstalledModesFromYaml(content: string, out: Record): void { try { const parsed = yaml.parse(content) const result = customModesSettingsSchema.safeParse(parsed) @@ -266,7 +269,7 @@ export class MarketplaceManager { } for (const mode of result.data.customModes) { if (this.isMarketplaceInstalledMode(mode)) { - out[mode.marketplaceItemId!] = { type: "mode" } + out[mode.marketplaceItemId] = { type: "mode" } } } } catch { diff --git a/src/services/marketplace/SimpleInstaller.ts b/src/services/marketplace/SimpleInstaller.ts index 960dcedc0b..ac53c14fb8 100644 --- a/src/services/marketplace/SimpleInstaller.ts +++ b/src/services/marketplace/SimpleInstaller.ts @@ -2,7 +2,7 @@ import * as vscode from "vscode" import * as path from "path" import * as fs from "fs/promises" import * as yaml from "yaml" -import type { MarketplaceItem, MarketplaceItemType, InstallMarketplaceItemOptions, McpParameter, ModeConfig } from "@roo-code/types" +import type { MarketplaceItem, McpParameter, ModeConfig } from "@roo-code/types" import { GlobalFileNames } from "../../shared/globalFileNames" import { ensureSettingsDirectoryExists } from "../../utils/globalContext" import type { CustomModesManager } from "../../core/config/CustomModesManager" @@ -22,6 +22,7 @@ export class SimpleInstaller { async installItem(item: MarketplaceItem, options: InstallOptions): Promise<{ filePath: string; line?: number }> { const { target } = options + const itemType = item.type switch (item.type) { case "mode": @@ -29,7 +30,7 @@ export class SimpleInstaller { case "mcp": return await this.installMcp(item, target, options) default: - throw new Error("Unsupported item type") + throw new Error(`Unsupported item type: ${itemType}`) } } @@ -293,6 +294,7 @@ export class SimpleInstaller { async removeItem(item: MarketplaceItem, options: InstallOptions): Promise { const { target } = options + const itemType = item.type switch (item.type) { case "mode": @@ -302,7 +304,7 @@ export class SimpleInstaller { await this.removeMcp(item, target) break default: - throw new Error("Unsupported item type") + throw new Error(`Unsupported item type: ${itemType}`) } } @@ -343,16 +345,12 @@ export class SimpleInstaller { ) if (!candidate) { - throw new Error(t("common:customModes.errors.modeNotFound")) + const msg = t("marketplace:installation.notInstalledForTarget") + throw new Error(msg === "installation.notInstalledForTarget" ? "Mode not found" : msg) } // Delete only from the selected source to avoid unintended removals - if (typeof (this.customModesManager as any).deleteCustomModeForSource === "function") { - await (this.customModesManager as any).deleteCustomModeForSource(modeSlug, target, true) - } else { - // Scoped deletion not supported in this version - throw new Error("Scoped deletion is not supported in this version") - } + await this.customModesManager.deleteCustomModeForSource(modeSlug, target, true) } private async removeMcp(item: MarketplaceItem, target: "project" | "global"): Promise { From fd85c22b5cb2bbc28a760711b8efd3666055b775 Mon Sep 17 00:00:00 2001 From: matt-rudolph Date: Tue, 23 Sep 2025 16:47:31 -0600 Subject: [PATCH 6/6] chore(i18n): fix marketplace translations across locales for PR #8181 --- src/i18n/locales/ca/marketplace.json | 3 ++- src/i18n/locales/de/marketplace.json | 3 ++- src/i18n/locales/es/marketplace.json | 3 ++- src/i18n/locales/fr/marketplace.json | 3 ++- src/i18n/locales/hi/marketplace.json | 3 ++- src/i18n/locales/id/marketplace.json | 3 ++- src/i18n/locales/it/marketplace.json | 3 ++- src/i18n/locales/ja/marketplace.json | 3 ++- src/i18n/locales/ko/marketplace.json | 3 ++- src/i18n/locales/nl/marketplace.json | 3 ++- src/i18n/locales/pl/marketplace.json | 3 ++- src/i18n/locales/pt-BR/marketplace.json | 3 ++- src/i18n/locales/ru/marketplace.json | 3 ++- src/i18n/locales/tr/marketplace.json | 3 ++- src/i18n/locales/vi/marketplace.json | 3 ++- src/i18n/locales/zh-CN/marketplace.json | 3 ++- src/i18n/locales/zh-TW/marketplace.json | 3 ++- 17 files changed, 34 insertions(+), 17 deletions(-) diff --git a/src/i18n/locales/ca/marketplace.json b/src/i18n/locales/ca/marketplace.json index cf0710ac97..ca4303f943 100644 --- a/src/i18n/locales/ca/marketplace.json +++ b/src/i18n/locales/ca/marketplace.json @@ -64,6 +64,7 @@ "installError": "Error en instal·lar \"{{itemName}}\": {{errorMessage}}", "removing": "Eliminant element: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" eliminat correctament", - "removeError": "Error en eliminar \"{{itemName}}\": {{errorMessage}}" + "removeError": "Error en eliminar \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Mode no trobat" } } diff --git a/src/i18n/locales/de/marketplace.json b/src/i18n/locales/de/marketplace.json index dea82aa44c..2e9e771f60 100644 --- a/src/i18n/locales/de/marketplace.json +++ b/src/i18n/locales/de/marketplace.json @@ -64,6 +64,7 @@ "installError": "Installation von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}", "removing": "Element wird entfernt: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" erfolgreich entfernt", - "removeError": "Entfernung von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}" + "removeError": "Entfernung von \"{{itemName}}\" fehlgeschlagen: {{errorMessage}}", + "notInstalledForTarget": "Modus nicht gefunden" } } diff --git a/src/i18n/locales/es/marketplace.json b/src/i18n/locales/es/marketplace.json index ecee7c1645..aecc70f763 100644 --- a/src/i18n/locales/es/marketplace.json +++ b/src/i18n/locales/es/marketplace.json @@ -64,6 +64,7 @@ "installError": "Error al instalar \"{{itemName}}\": {{errorMessage}}", "removing": "Eliminando elemento: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" eliminado correctamente", - "removeError": "Error al eliminar \"{{itemName}}\": {{errorMessage}}" + "removeError": "Error al eliminar \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Modo no encontrado" } } diff --git a/src/i18n/locales/fr/marketplace.json b/src/i18n/locales/fr/marketplace.json index 7a6a3dbd74..bcf5d793b2 100644 --- a/src/i18n/locales/fr/marketplace.json +++ b/src/i18n/locales/fr/marketplace.json @@ -64,6 +64,7 @@ "installError": "Échec de l'installation de \"{{itemName}}\" : {{errorMessage}}", "removing": "Suppression de l'élément : \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" supprimé avec succès", - "removeError": "Échec de la suppression de \"{{itemName}}\" : {{errorMessage}}" + "removeError": "Échec de la suppression de \"{{itemName}}\" : {{errorMessage}}", + "notInstalledForTarget": "Mode non trouvé" } } diff --git a/src/i18n/locales/hi/marketplace.json b/src/i18n/locales/hi/marketplace.json index 3797c50b01..e30f95d6d9 100644 --- a/src/i18n/locales/hi/marketplace.json +++ b/src/i18n/locales/hi/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" इंस्टॉल करने में विफल: {{errorMessage}}", "removing": "आइटम हटा रहे हैं: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" सफलतापूर्वक हटाया गया", - "removeError": "\"{{itemName}}\" हटाने में विफल: {{errorMessage}}" + "removeError": "\"{{itemName}}\" हटाने में विफल: {{errorMessage}}", + "notInstalledForTarget": "मोड नहीं मिला" } } diff --git a/src/i18n/locales/id/marketplace.json b/src/i18n/locales/id/marketplace.json index 7f31c78cbf..1194d54eb6 100644 --- a/src/i18n/locales/id/marketplace.json +++ b/src/i18n/locales/id/marketplace.json @@ -64,6 +64,7 @@ "installError": "Gagal menginstal \"{{itemName}}\": {{errorMessage}}", "removing": "Menghapus item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" berhasil dihapus", - "removeError": "Gagal menghapus \"{{itemName}}\": {{errorMessage}}" + "removeError": "Gagal menghapus \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Mode tidak ditemukan" } } diff --git a/src/i18n/locales/it/marketplace.json b/src/i18n/locales/it/marketplace.json index 46c50acc7d..fa80eb78f1 100644 --- a/src/i18n/locales/it/marketplace.json +++ b/src/i18n/locales/it/marketplace.json @@ -64,6 +64,7 @@ "installError": "Installazione di \"{{itemName}}\" fallita: {{errorMessage}}", "removing": "Rimozione elemento: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" rimosso con successo", - "removeError": "Rimozione di \"{{itemName}}\" fallita: {{errorMessage}}" + "removeError": "Rimozione di \"{{itemName}}\" fallita: {{errorMessage}}", + "notInstalledForTarget": "Modalità non trovata" } } diff --git a/src/i18n/locales/ja/marketplace.json b/src/i18n/locales/ja/marketplace.json index 426a5db1fd..a7a7156f0c 100644 --- a/src/i18n/locales/ja/marketplace.json +++ b/src/i18n/locales/ja/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\"のインストールに失敗しました: {{errorMessage}}", "removing": "アイテムを削除中: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\"の削除が完了しました", - "removeError": "\"{{itemName}}\"の削除に失敗しました: {{errorMessage}}" + "removeError": "\"{{itemName}}\"の削除に失敗しました: {{errorMessage}}", + "notInstalledForTarget": "モードが見つかりません" } } diff --git a/src/i18n/locales/ko/marketplace.json b/src/i18n/locales/ko/marketplace.json index 349f0c5ad8..61c98e940a 100644 --- a/src/i18n/locales/ko/marketplace.json +++ b/src/i18n/locales/ko/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" 설치 실패: {{errorMessage}}", "removing": "항목 제거 중: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" 제거 완료", - "removeError": "\"{{itemName}}\" 제거 실패: {{errorMessage}}" + "removeError": "\"{{itemName}}\" 제거 실패: {{errorMessage}}", + "notInstalledForTarget": "모드를 찾을 수 없습니다" } } diff --git a/src/i18n/locales/nl/marketplace.json b/src/i18n/locales/nl/marketplace.json index 1274666b9c..609ed2d3c5 100644 --- a/src/i18n/locales/nl/marketplace.json +++ b/src/i18n/locales/nl/marketplace.json @@ -64,6 +64,7 @@ "installError": "Installatie van \"{{itemName}}\" mislukt: {{errorMessage}}", "removing": "Item verwijderen: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" succesvol verwijderd", - "removeError": "Verwijdering van \"{{itemName}}\" mislukt: {{errorMessage}}" + "removeError": "Verwijdering van \"{{itemName}}\" mislukt: {{errorMessage}}", + "notInstalledForTarget": "Modus niet gevonden" } } diff --git a/src/i18n/locales/pl/marketplace.json b/src/i18n/locales/pl/marketplace.json index 3ba3c076ad..d2a77a030e 100644 --- a/src/i18n/locales/pl/marketplace.json +++ b/src/i18n/locales/pl/marketplace.json @@ -64,6 +64,7 @@ "installError": "Instalacja \"{{itemName}}\" nie powiodła się: {{errorMessage}}", "removing": "Usuwanie elementu: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" usunięto pomyślnie", - "removeError": "Usunięcie \"{{itemName}}\" nie powiodło się: {{errorMessage}}" + "removeError": "Usunięcie \"{{itemName}}\" nie powiodło się: {{errorMessage}}", + "notInstalledForTarget": "Nie znaleziono trybu" } } diff --git a/src/i18n/locales/pt-BR/marketplace.json b/src/i18n/locales/pt-BR/marketplace.json index b0e763b0ff..b5a0ab887f 100644 --- a/src/i18n/locales/pt-BR/marketplace.json +++ b/src/i18n/locales/pt-BR/marketplace.json @@ -64,6 +64,7 @@ "installError": "Falha ao instalar \"{{itemName}}\": {{errorMessage}}", "removing": "Removendo item: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" removido com sucesso", - "removeError": "Falha ao remover \"{{itemName}}\": {{errorMessage}}" + "removeError": "Falha ao remover \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Modo não encontrado" } } diff --git a/src/i18n/locales/ru/marketplace.json b/src/i18n/locales/ru/marketplace.json index 1c4ba45df5..3f53914772 100644 --- a/src/i18n/locales/ru/marketplace.json +++ b/src/i18n/locales/ru/marketplace.json @@ -64,6 +64,7 @@ "installError": "Не удалось установить \"{{itemName}}\": {{errorMessage}}", "removing": "Удаление элемента: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" успешно удален", - "removeError": "Не удалось удалить \"{{itemName}}\": {{errorMessage}}" + "removeError": "Не удалось удалить \"{{itemName}}\": {{errorMessage}}", + "notInstalledForTarget": "Режим не найден" } } diff --git a/src/i18n/locales/tr/marketplace.json b/src/i18n/locales/tr/marketplace.json index 628c239a12..f60ec3c6e8 100644 --- a/src/i18n/locales/tr/marketplace.json +++ b/src/i18n/locales/tr/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" yüklenemedi: {{errorMessage}}", "removing": "Öğe kaldırılıyor: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" başarıyla kaldırıldı", - "removeError": "\"{{itemName}}\" kaldırılamadı: {{errorMessage}}" + "removeError": "\"{{itemName}}\" kaldırılamadı: {{errorMessage}}", + "notInstalledForTarget": "Mod bulunamadı" } } diff --git a/src/i18n/locales/vi/marketplace.json b/src/i18n/locales/vi/marketplace.json index 8fbc1a7ad0..f6cda48e35 100644 --- a/src/i18n/locales/vi/marketplace.json +++ b/src/i18n/locales/vi/marketplace.json @@ -64,6 +64,7 @@ "installError": "Cài đặt \"{{itemName}}\" thất bại: {{errorMessage}}", "removing": "Đang xóa mục: \"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" đã được xóa thành công", - "removeError": "Xóa \"{{itemName}}\" thất bại: {{errorMessage}}" + "removeError": "Xóa \"{{itemName}}\" thất bại: {{errorMessage}}", + "notInstalledForTarget": "Không tìm thấy chế độ" } } diff --git a/src/i18n/locales/zh-CN/marketplace.json b/src/i18n/locales/zh-CN/marketplace.json index a69e9c1e9d..06c1d0463d 100644 --- a/src/i18n/locales/zh-CN/marketplace.json +++ b/src/i18n/locales/zh-CN/marketplace.json @@ -64,6 +64,7 @@ "installError": "\"{{itemName}}\" 安装失败:{{errorMessage}}", "removing": "正在移除项目:\"{{itemName}}\"", "removeSuccess": "\"{{itemName}}\" 移除成功", - "removeError": "\"{{itemName}}\" 移除失败:{{errorMessage}}" + "removeError": "\"{{itemName}}\" 移除失败:{{errorMessage}}", + "notInstalledForTarget": "未找到模式" } } diff --git a/src/i18n/locales/zh-TW/marketplace.json b/src/i18n/locales/zh-TW/marketplace.json index 9270314108..37c4e8f760 100644 --- a/src/i18n/locales/zh-TW/marketplace.json +++ b/src/i18n/locales/zh-TW/marketplace.json @@ -64,6 +64,7 @@ "installError": "「{{itemName}}」安裝失敗:{{errorMessage}}", "removing": "正在移除項目:「{{itemName}}」", "removeSuccess": "「{{itemName}}」移除成功", - "removeError": "「{{itemName}}」移除失敗:{{errorMessage}}" + "removeError": "「{{itemName}}」移除失敗:{{errorMessage}}", + "notInstalledForTarget": "找不到模式" } }