Selaa lähdekoodia

fix: handle YAML parsing edge cases in CustomModesManager (#5099)

* fix: handle YAML parsing edge cases in CustomModesManager

- Add BOM (Byte Order Mark) stripping for UTF-8 and UTF-16
- Normalize invisible characters including non-breaking spaces
- Replace fancy quotes and dashes with standard characters
- Remove zero-width characters that can cause parsing issues
- Add comprehensive test coverage for all edge cases

This fixes the YAML parsing limitations documented in PR #237 by
implementing proper preprocessing before parsing YAML content.

* fix: address PR review comments

- Fix BOM handling to correctly handle UTF-16 (all BOMs appear as \uFEFF when decoded)
- Optimize cleanInvisibleCharacters with single regex pass for better performance
- Prevent duplicate error messages by marking errors as already handled
- Refactor test file to use mockFsReadFile helper function to reduce duplication
- Fix YAML indentation in tests (use spaces instead of tabs)
- Add ESLint disable comment for character class warning (regex is correct)

* fix: prevent YAML line breaks by setting lineWidth to 0

- Added lineWidth: 0 option to all yaml.stringify() calls
- Prevents automatic line wrapping at 80 characters
- Improves readability of YAML output for long strings
- Applied to CustomModesManager, SimpleInstaller, and migrateSettings

* fix: add defaultStringType option to yaml.stringify calls

- Added defaultStringType: 'PLAIN' to minimize formatting changes
- This helps preserve plain scalars when possible
- Works alongside lineWidth: 0 to prevent automatic line wrapping

* refactor: extract problematic characters regex as a named constant

- Move regex pattern to PROBLEMATIC_CHARS_REGEX static constant
- Add comprehensive documentation for each character range
- Improves maintainability and makes the pattern reusable

* test: add comprehensive edge case tests for YAML parsing

- Add test for mixed line endings (CRLF vs LF)
- Add test for multiple BOMs in sequence
- Add test for deeply nested structures with problematic characters
- Ensures robustness across different real-world scenarios

* feat(i18n): add error messages for custom modes in multiple languages

* fix: update tests to expect i18n keys instead of hardcoded strings

- Update CustomModesManager tests to expect translation keys
- Fix YAML edge case tests to match new i18n error messages
- All tests now pass with the i18n integration

* refactor: use strip-bom package and fix error handling

- Replace custom stripBOM method with existing strip-bom package
- Fix duplicate error handling in parseYamlSafely by returning empty object instead of re-throwing
- Addresses review comments from PR #5099

---------

Co-authored-by: Daniel Riccio <[email protected]>
Hannes Rudolph 6 kuukautta sitten
vanhempi
sitoutus
e559beee67

+ 107 - 21
src/core/config/CustomModesManager.ts

@@ -3,6 +3,7 @@ import * as path from "path"
 import * as fs from "fs/promises"
 
 import * as yaml from "yaml"
+import stripBom from "strip-bom"
 
 import { type ModeConfig, customModesSettingsSchema } from "@roo-code/types"
 
@@ -11,6 +12,7 @@ import { getWorkspacePath } from "../../utils/path"
 import { logger } from "../../utils/logging"
 import { GlobalFileNames } from "../../shared/globalFileNames"
 import { ensureSettingsDirectoryExists } from "../../utils/globalContext"
+import { t } from "../../i18n"
 
 const ROOMODES_FILENAME = ".roomodes"
 
@@ -73,12 +75,88 @@ export class CustomModesManager {
 		return exists ? roomodesPath : undefined
 	}
 
+	/**
+	 * Regex pattern for problematic characters that need to be cleaned from YAML content
+	 * Includes:
+	 * - \u00A0: Non-breaking space
+	 * - \u200B-\u200D: Zero-width spaces and joiners
+	 * - \u2010-\u2015, \u2212: Various dash characters
+	 * - \u2018-\u2019: Smart single quotes
+	 * - \u201C-\u201D: Smart double quotes
+	 */
+	private static readonly PROBLEMATIC_CHARS_REGEX =
+		// eslint-disable-next-line no-misleading-character-class
+		/[\u00A0\u200B\u200C\u200D\u2010\u2011\u2012\u2013\u2014\u2015\u2212\u2018\u2019\u201C\u201D]/g
+
+	/**
+	 * Clean invisible and problematic characters from YAML content
+	 */
+	private cleanInvisibleCharacters(content: string): string {
+		// Single pass replacement for all problematic characters
+		return content.replace(CustomModesManager.PROBLEMATIC_CHARS_REGEX, (match) => {
+			switch (match) {
+				case "\u00A0": // Non-breaking space
+					return " "
+				case "\u200B": // Zero-width space
+				case "\u200C": // Zero-width non-joiner
+				case "\u200D": // Zero-width joiner
+					return ""
+				case "\u2018": // Left single quotation mark
+				case "\u2019": // Right single quotation mark
+					return "'"
+				case "\u201C": // Left double quotation mark
+				case "\u201D": // Right double quotation mark
+					return '"'
+				default: // Dash characters (U+2010 through U+2015, U+2212)
+					return "-"
+			}
+		})
+	}
+
+	/**
+	 * Parse YAML content with enhanced error handling and preprocessing
+	 */
+	private parseYamlSafely(content: string, filePath: string): any {
+		// Clean the content
+		let cleanedContent = stripBom(content)
+		cleanedContent = this.cleanInvisibleCharacters(cleanedContent)
+
+		try {
+			return yaml.parse(cleanedContent)
+		} catch (error) {
+			const errorMsg = error instanceof Error ? error.message : String(error)
+			console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, errorMsg)
+
+			// Show user-friendly error message for .roomodes files
+			if (filePath.endsWith(ROOMODES_FILENAME)) {
+				const lineMatch = errorMsg.match(/at line (\d+)/)
+				const line = lineMatch ? lineMatch[1] : "unknown"
+				vscode.window.showErrorMessage(t("common:customModes.errors.yamlParseError", { line }))
+			}
+
+			// Return empty object to prevent duplicate error handling
+			return {}
+		}
+	}
+
 	private async loadModesFromFile(filePath: string): Promise<ModeConfig[]> {
 		try {
 			const content = await fs.readFile(filePath, "utf-8")
-			const settings = yaml.parse(content)
+			const settings = this.parseYamlSafely(content, filePath)
 			const result = customModesSettingsSchema.safeParse(settings)
+
 			if (!result.success) {
+				console.error(`[CustomModesManager] Schema validation failed for ${filePath}:`, result.error)
+
+				// Show user-friendly error for .roomodes files
+				if (filePath.endsWith(ROOMODES_FILENAME)) {
+					const issues = result.error.issues
+						.map((issue) => `• ${issue.path.join(".")}: ${issue.message}`)
+						.join("\n")
+
+					vscode.window.showErrorMessage(t("common:customModes.errors.schemaValidationError", { issues }))
+				}
+
 				return []
 			}
 
@@ -89,8 +167,11 @@ export class CustomModesManager {
 			// Add source to each mode
 			return result.data.customModes.map((mode) => ({ ...mode, source }))
 		} catch (error) {
-			const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}`
-			console.error(`[CustomModesManager] ${errorMsg}`)
+			// Only log if the error wasn't already handled in parseYamlSafely
+			if (!(error as any).alreadyHandled) {
+				const errorMsg = `Failed to load modes from ${filePath}: ${error instanceof Error ? error.message : String(error)}`
+				console.error(`[CustomModesManager] ${errorMsg}`)
+			}
 			return []
 		}
 	}
@@ -124,7 +205,12 @@ export class CustomModesManager {
 		const fileExists = await fileExistsAtPath(filePath)
 
 		if (!fileExists) {
-			await this.queueWrite(() => fs.writeFile(filePath, yaml.stringify({ customModes: [] })))
+			await this.queueWrite(() =>
+				fs.writeFile(
+					filePath,
+					yaml.stringify({ customModes: [] }, { lineWidth: 0, defaultStringType: "PLAIN" }),
+				),
+			)
 		}
 
 		return filePath
@@ -147,13 +233,12 @@ export class CustomModesManager {
 				await this.getCustomModesFilePath()
 				const content = await fs.readFile(settingsPath, "utf-8")
 
-				const errorMessage =
-					"Invalid custom modes format. Please ensure your settings follow the correct YAML format."
+				const errorMessage = t("common:customModes.errors.invalidFormat")
 
 				let config: any
 
 				try {
-					config = yaml.parse(content)
+					config = this.parseYamlSafely(content, settingsPath)
 				} catch (error) {
 					console.error(error)
 					vscode.window.showErrorMessage(errorMessage)
@@ -284,7 +369,7 @@ export class CustomModesManager {
 
 				if (!workspaceFolders || workspaceFolders.length === 0) {
 					logger.error("Failed to update project mode: No workspace folder found", { slug })
-					throw new Error("No workspace folder found for project-specific mode")
+					throw new Error(t("common:customModes.errors.noWorkspaceForProject"))
 				}
 
 				const workspaceRoot = getWorkspacePath()
@@ -318,7 +403,7 @@ export class CustomModesManager {
 		} catch (error) {
 			const errorMessage = error instanceof Error ? error.message : String(error)
 			logger.error("Failed to update custom mode", { slug, error: errorMessage })
-			vscode.window.showErrorMessage(`Failed to update custom mode: ${errorMessage}`)
+			vscode.window.showErrorMessage(t("common:customModes.errors.updateFailed", { error: errorMessage }))
 		}
 	}
 
@@ -329,20 +414,20 @@ export class CustomModesManager {
 			content = await fs.readFile(filePath, "utf-8")
 		} catch (error) {
 			// File might not exist yet.
-			content = yaml.stringify({ customModes: [] })
+			content = yaml.stringify({ customModes: [] }, { lineWidth: 0, defaultStringType: "PLAIN" })
 		}
 
 		let settings
 
 		try {
-			settings = yaml.parse(content)
+			settings = this.parseYamlSafely(content, filePath)
 		} catch (error) {
-			console.error(`[CustomModesManager] Failed to parse YAML from ${filePath}:`, error)
+			// Error already logged in parseYamlSafely
 			settings = { customModes: [] }
 		}
 
 		settings.customModes = operation(settings.customModes || [])
-		await fs.writeFile(filePath, yaml.stringify(settings), "utf-8")
+		await fs.writeFile(filePath, yaml.stringify(settings, { lineWidth: 0, defaultStringType: "PLAIN" }), "utf-8")
 	}
 
 	private async refreshMergedState(): Promise<void> {
@@ -373,7 +458,7 @@ export class CustomModesManager {
 			const globalMode = settingsModes.find((m) => m.slug === slug)
 
 			if (!projectMode && !globalMode) {
-				throw new Error("Write error: Mode not found")
+				throw new Error(t("common:customModes.errors.modeNotFound"))
 			}
 
 			await this.queueWrite(async () => {
@@ -392,23 +477,24 @@ export class CustomModesManager {
 				await this.refreshMergedState()
 			})
 		} catch (error) {
-			vscode.window.showErrorMessage(
-				`Failed to delete custom mode: ${error instanceof Error ? error.message : String(error)}`,
-			)
+			const errorMessage = error instanceof Error ? error.message : String(error)
+			vscode.window.showErrorMessage(t("common:customModes.errors.deleteFailed", { error: errorMessage }))
 		}
 	}
 
 	public async resetCustomModes(): Promise<void> {
 		try {
 			const filePath = await this.getCustomModesFilePath()
-			await fs.writeFile(filePath, yaml.stringify({ customModes: [] }))
+			await fs.writeFile(
+				filePath,
+				yaml.stringify({ customModes: [] }, { lineWidth: 0, defaultStringType: "PLAIN" }),
+			)
 			await this.context.globalState.update("customModes", [])
 			this.clearCache()
 			await this.onUpdate()
 		} catch (error) {
-			vscode.window.showErrorMessage(
-				`Failed to reset custom modes: ${error instanceof Error ? error.message : String(error)}`,
-			)
+			const errorMessage = error instanceof Error ? error.message : String(error)
+			vscode.window.showErrorMessage(t("common:customModes.errors.resetFailed", { error: errorMessage }))
 		}
 	}
 

+ 1 - 1
src/core/config/__tests__/CustomModesManager.spec.ts

@@ -754,7 +754,7 @@ describe("CustomModesManager", () => {
 
 			await manager.deleteCustomMode("non-existent-mode")
 
-			expect(mockShowError).toHaveBeenCalledWith(expect.stringContaining("Write error"))
+			expect(mockShowError).toHaveBeenCalledWith("customModes.errors.deleteFailed")
 		})
 	})
 

+ 474 - 0
src/core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts

@@ -0,0 +1,474 @@
+// npx vitest core/config/__tests__/CustomModesManager.yamlEdgeCases.spec.ts
+
+import type { Mock } from "vitest"
+
+import * as path from "path"
+import * as fs from "fs/promises"
+
+import * as yaml from "yaml"
+import * as vscode from "vscode"
+
+import type { ModeConfig } from "@roo-code/types"
+
+import { fileExistsAtPath } from "../../../utils/fs"
+import { getWorkspacePath } from "../../../utils/path"
+import { GlobalFileNames } from "../../../shared/globalFileNames"
+
+import { CustomModesManager } from "../CustomModesManager"
+
+vi.mock("vscode", () => ({
+	workspace: {
+		workspaceFolders: [],
+		onDidSaveTextDocument: vi.fn(),
+		createFileSystemWatcher: vi.fn(),
+	},
+	window: {
+		showErrorMessage: vi.fn(),
+	},
+}))
+
+vi.mock("fs/promises")
+
+vi.mock("../../../utils/fs")
+vi.mock("../../../utils/path")
+
+describe("CustomModesManager - YAML Edge Cases", () => {
+	let manager: CustomModesManager
+	let mockContext: vscode.ExtensionContext
+	let mockOnUpdate: Mock
+	let mockWorkspaceFolders: { uri: { fsPath: string } }[]
+
+	const mockStoragePath = `${path.sep}mock${path.sep}settings`
+	const mockSettingsPath = path.join(mockStoragePath, "settings", GlobalFileNames.customModes)
+	const mockRoomodes = `${path.sep}mock${path.sep}workspace${path.sep}.roomodes`
+
+	// Helper function to reduce duplication in fs.readFile mocks
+	const mockFsReadFile = (files: Record<string, string>) => {
+		;(fs.readFile as Mock).mockImplementation(async (path: string) => {
+			if (files[path]) return files[path]
+			throw new Error("File not found")
+		})
+	}
+
+	beforeEach(() => {
+		mockOnUpdate = vi.fn()
+		mockContext = {
+			globalState: {
+				get: vi.fn(),
+				update: vi.fn(),
+				keys: vi.fn(() => []),
+				setKeysForSync: vi.fn(),
+			},
+			globalStorageUri: {
+				fsPath: mockStoragePath,
+			},
+		} as unknown as vscode.ExtensionContext
+
+		mockWorkspaceFolders = [{ uri: { fsPath: "/mock/workspace" } }]
+		;(vscode.workspace as any).workspaceFolders = mockWorkspaceFolders
+		;(vscode.workspace.onDidSaveTextDocument as Mock).mockReturnValue({ dispose: vi.fn() })
+		;(getWorkspacePath as Mock).mockReturnValue("/mock/workspace")
+		;(fileExistsAtPath as Mock).mockImplementation(async (path: string) => {
+			return path === mockSettingsPath || path === mockRoomodes
+		})
+		;(fs.mkdir as Mock).mockResolvedValue(undefined)
+		;(fs.readFile as Mock).mockImplementation(async (path: string) => {
+			if (path === mockSettingsPath) {
+				return yaml.stringify({ customModes: [] })
+			}
+			throw new Error("File not found")
+		})
+
+		// Mock createFileSystemWatcher to prevent file watching in tests
+		const mockWatcher = {
+			onDidChange: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+			onDidCreate: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+			onDidDelete: vi.fn().mockReturnValue({ dispose: vi.fn() }),
+			dispose: vi.fn(),
+		}
+		;(vscode.workspace.createFileSystemWatcher as Mock).mockReturnValue(mockWatcher)
+
+		manager = new CustomModesManager(mockContext, mockOnUpdate)
+	})
+
+	afterEach(() => {
+		vi.clearAllMocks()
+	})
+
+	describe("BOM (Byte Order Mark) handling", () => {
+		it("should handle UTF-8 BOM in YAML files", async () => {
+			const yamlWithBOM =
+				"\uFEFF" +
+				yaml.stringify({
+					customModes: [
+						{
+							slug: "test-mode",
+							name: "Test Mode",
+							roleDefinition: "Test role",
+							groups: ["read"],
+						},
+					],
+				})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithBOM,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].slug).toBe("test-mode")
+			expect(modes[0].name).toBe("Test Mode")
+		})
+
+		it("should handle UTF-16 BOM in YAML files", async () => {
+			// When Node.js reads UTF-16 files, the BOM is correctly decoded as \uFEFF
+			const yamlWithBOM =
+				"\uFEFF" +
+				yaml.stringify({
+					customModes: [
+						{
+							slug: "utf16-mode",
+							name: "UTF-16 Mode",
+							roleDefinition: "Test role",
+							groups: ["read"],
+						},
+					],
+				})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithBOM,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].slug).toBe("utf16-mode")
+		})
+	})
+
+	describe("Invisible character handling", () => {
+		it("should handle non-breaking spaces in YAML", async () => {
+			// YAML with non-breaking spaces (U+00A0) instead of regular spaces
+			const yamlWithNonBreakingSpaces = `customModes:
+  - slug: "test-mode"
+    name: "Test\u00A0Mode"
+    roleDefinition: "Test\u00A0role\u00A0with\u00A0non-breaking\u00A0spaces"
+    groups: ["read"]`
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithNonBreakingSpaces,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].name).toBe("Test Mode") // Non-breaking spaces replaced with regular spaces
+			expect(modes[0].roleDefinition).toBe("Test role with non-breaking spaces")
+		})
+
+		it("should handle zero-width characters", async () => {
+			// YAML with zero-width characters
+			const yamlWithZeroWidth = `customModes:
+  - slug: "test-mode"
+    name: "Test\u200BMode\u200C"
+    roleDefinition: "Test\u200Drole"
+    groups: ["read"]`
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithZeroWidth,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].name).toBe("TestMode") // Zero-width characters removed
+			expect(modes[0].roleDefinition).toBe("Testrole")
+		})
+
+		it("should normalize various quote characters", async () => {
+			// Use fancy quotes that will be normalized before YAML parsing
+			// The fancy quotes will be normalized to standard quotes
+			const yamlWithFancyQuotes = yaml.stringify({
+				customModes: [
+					{
+						slug: "test-mode",
+						name: "Test Mode",
+						roleDefinition: "Test role with \u2018fancy\u2019 quotes and \u201Ccurly\u201D quotes",
+						groups: ["read"],
+					},
+				],
+			})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithFancyQuotes,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].roleDefinition).toBe("Test role with 'fancy' quotes and \"curly\" quotes")
+		})
+	})
+
+	// Note: YAML anchor/alias support has been removed to reduce complexity
+	// If needed in the future, users should pre-process their YAML files
+
+	describe("Complex fileRegex handling", () => {
+		it("should handle complex fileRegex syntax gracefully", async () => {
+			const yamlWithComplexFileRegex = yaml.stringify({
+				customModes: [
+					{
+						slug: "test-mode",
+						name: "Test Mode",
+						roleDefinition: "Test role",
+						groups: [
+							"read",
+							["edit", { fileRegex: "\\.md$", description: "Markdown files only" }],
+							"browser",
+						],
+					},
+				],
+			})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithComplexFileRegex,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			// Should successfully parse the complex fileRegex syntax
+			expect(modes).toHaveLength(1)
+			expect(modes[0].groups).toHaveLength(3)
+			expect(modes[0].groups[1]).toEqual(["edit", { fileRegex: "\\.md$", description: "Markdown files only" }])
+		})
+
+		it("should handle invalid fileRegex syntax with clear error", async () => {
+			// This YAML has invalid structure that might cause parsing issues
+			const invalidYaml = `customModes:
+	 - slug: "test-mode"
+	   name: "Test Mode"
+	   roleDefinition: "Test role"
+	   groups:
+	     - read
+	     - ["edit", { fileRegex: "\\.md$" }]  # This line has invalid YAML syntax
+	     - browser`
+
+			mockFsReadFile({
+				[mockRoomodes]: invalidYaml,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			// Should handle the error gracefully
+			expect(modes).toHaveLength(0)
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.yamlParseError")
+		})
+	})
+
+	describe("Error messages", () => {
+		it("should provide detailed syntax error messages with context", async () => {
+			const invalidYaml = `customModes:
+	 - slug: "test-mode"
+	   name: "Test Mode"
+	   roleDefinition: "Test role
+	   groups: ["read"]` // Missing closing quote
+
+			mockFsReadFile({
+				[mockRoomodes]: invalidYaml,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			// Should fallback to empty array and show detailed error
+			expect(modes).toHaveLength(0)
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.yamlParseError")
+		})
+
+		it("should provide schema validation error messages", async () => {
+			const invalidSchema = yaml.stringify({
+				customModes: [
+					{
+						slug: "test-mode",
+						name: "Test Mode",
+						// Missing required 'roleDefinition' field
+						groups: ["read"],
+					},
+				],
+			})
+
+			mockFsReadFile({
+				[mockRoomodes]: invalidSchema,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			// Should show schema validation error
+			expect(modes).toHaveLength(0)
+			expect(vscode.window.showErrorMessage).toHaveBeenCalledWith("customModes.errors.schemaValidationError")
+		})
+	})
+
+	describe("UTF-8 encoding", () => {
+		it("should handle special characters and emojis", async () => {
+			const yamlWithEmojis = yaml.stringify({
+				customModes: [
+					{
+						slug: "emoji-mode",
+						name: "📝 Writing Mode",
+						roleDefinition: "A mode for writing with emojis 🚀",
+						groups: ["read", "edit"],
+					},
+				],
+			})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithEmojis,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].name).toBe("📝 Writing Mode")
+			expect(modes[0].roleDefinition).toBe("A mode for writing with emojis 🚀")
+		})
+
+		it("should handle various international characters", async () => {
+			const yamlWithInternational = yaml.stringify({
+				customModes: [
+					{
+						slug: "intl-mode",
+						name: "Mode Français",
+						roleDefinition: "Mode für Deutsch, 日本語モード, Режим русский",
+						groups: ["read"],
+					},
+				],
+			})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithInternational,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].roleDefinition).toContain("für Deutsch")
+			expect(modes[0].roleDefinition).toContain("日本語モード")
+			expect(modes[0].roleDefinition).toContain("Режим русский")
+		})
+	})
+
+	describe("Additional edge cases", () => {
+		it("should handle mixed line endings (CRLF vs LF)", async () => {
+			// YAML with mixed line endings
+			const yamlWithMixedLineEndings =
+				"customModes:\r\n" +
+				'  - slug: "test-mode"\n' +
+				'    name: "Test Mode"\r\n' +
+				'    roleDefinition: "Test role with mixed line endings"\n' +
+				'    groups: ["read"]'
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithMixedLineEndings,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].slug).toBe("test-mode")
+			expect(modes[0].roleDefinition).toBe("Test role with mixed line endings")
+		})
+
+		it("should handle multiple BOMs in sequence", async () => {
+			// File with multiple BOMs (edge case from file concatenation)
+			const yamlWithMultipleBOMs =
+				"\uFEFF\uFEFF" +
+				yaml.stringify({
+					customModes: [
+						{
+							slug: "multi-bom-mode",
+							name: "Multi BOM Mode",
+							roleDefinition: "Test role",
+							groups: ["read"],
+						},
+					],
+				})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithMultipleBOMs,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].slug).toBe("multi-bom-mode")
+		})
+
+		it("should handle deeply nested structures with edge case characters", async () => {
+			const yamlWithComplexNesting = yaml.stringify({
+				customModes: [
+					{
+						slug: "complex-mode",
+						name: "Complex\u00A0Mode\u2019s Name",
+						roleDefinition: "Complex role with \u201Cquotes\u201D and \u2014dashes\u2014",
+						groups: [
+							"read",
+							[
+								"edit",
+								{
+									fileRegex: "\\.md$",
+									description: "Markdown files with \u2018special\u2019 chars",
+								},
+							],
+							[
+								"browser",
+								{
+									fileRegex: "\\.html?$",
+									description: "HTML files\u00A0only",
+								},
+							],
+						],
+					},
+				],
+			})
+
+			mockFsReadFile({
+				[mockRoomodes]: yamlWithComplexNesting,
+				[mockSettingsPath]: yaml.stringify({ customModes: [] }),
+			})
+
+			const modes = await manager.getCustomModes()
+
+			expect(modes).toHaveLength(1)
+			expect(modes[0].name).toBe("Complex Mode's Name")
+			expect(modes[0].roleDefinition).toBe('Complex role with "quotes" and -dashes-')
+			expect(modes[0].groups[1]).toEqual([
+				"edit",
+				{
+					fileRegex: "\\.md$",
+					description: "Markdown files with 'special' chars",
+				},
+			])
+			expect(modes[0].groups[2]).toEqual([
+				"browser",
+				{
+					fileRegex: "\\.html?$",
+					description: "HTML files only",
+				},
+			])
+		})
+	})
+})

+ 12 - 0
src/i18n/locales/ca/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "YAML no vàlid al fitxer .roomodes a la línia {{line}}. Comprova:\n• Indentació correcta (utilitza espais, no tabuladors)\n• Cometes i claudàtors coincidents\n• Sintaxi YAML vàlida",
+			"schemaValidationError": "Format de modes personalitzats no vàlid a .roomodes:\n{{issues}}",
+			"invalidFormat": "Format de modes personalitzats no vàlid. Assegura't que la teva configuració segueix el format YAML correcte.",
+			"updateFailed": "Error en actualitzar el mode personalitzat: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "La teva organització requereix autenticació de Roo Code Cloud. Si us plau, inicia sessió per continuar.",

+ 12 - 0
src/i18n/locales/de/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "Ungültiges YAML in .roomodes-Datei in Zeile {{line}}. Bitte überprüfe:\n• Korrekte Einrückung (verwende Leerzeichen, keine Tabs)\n• Passende Anführungszeichen und Klammern\n• Gültige YAML-Syntax",
+			"schemaValidationError": "Ungültiges Format für benutzerdefinierte Modi in .roomodes:\n{{issues}}",
+			"invalidFormat": "Ungültiges Format für benutzerdefinierte Modi. Bitte stelle sicher, dass deine Einstellungen dem korrekten YAML-Format folgen.",
+			"updateFailed": "Fehler beim Aktualisieren des benutzerdefinierten Modus: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Deine Organisation erfordert eine Roo Code Cloud-Authentifizierung. Bitte melde dich an, um fortzufahren.",

+ 12 - 0
src/i18n/locales/en/common.json

@@ -111,6 +111,18 @@
 		"task_prompt": "What should Roo do?",
 		"task_placeholder": "Type your task here"
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "Invalid YAML in .roomodes file at line {{line}}. Please check for:\n• Proper indentation (use spaces, not tabs)\n• Matching quotes and brackets\n• Valid YAML syntax",
+			"schemaValidationError": "Invalid custom modes format in .roomodes:\n{{issues}}",
+			"invalidFormat": "Invalid custom modes format. Please ensure your settings follow the correct YAML format.",
+			"updateFailed": "Failed to update custom mode: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Your organization requires Roo Code Cloud authentication. Please sign in to continue.",

+ 12 - 0
src/i18n/locales/es/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "YAML inválido en archivo .roomodes en línea {{line}}. Verifica:\n• Indentación correcta (usa espacios, no tabs)\n• Comillas y corchetes coincidentes\n• Sintaxis YAML válida",
+			"schemaValidationError": "Formato inválido de modos personalizados en .roomodes:\n{{issues}}",
+			"invalidFormat": "Formato inválido de modos personalizados. Asegúrate de que tu configuración siga el formato YAML correcto.",
+			"updateFailed": "Error al actualizar modo personalizado: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Tu organización requiere autenticación de Roo Code Cloud. Por favor, inicia sesión para continuar.",

+ 12 - 0
src/i18n/locales/fr/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "YAML invalide dans le fichier .roomodes à la ligne {{line}}. Vérifie :\n• L'indentation correcte (utilise des espaces, pas de tabulations)\n• Les guillemets et crochets correspondants\n• La syntaxe YAML valide",
+			"schemaValidationError": "Format invalide des modes personnalisés dans .roomodes :\n{{issues}}",
+			"invalidFormat": "Format invalide des modes personnalisés. Assure-toi que tes paramètres suivent le format YAML correct.",
+			"updateFailed": "Échec de la mise à jour du mode personnalisé : {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Votre organisation nécessite une authentification Roo Code Cloud. Veuillez vous connecter pour continuer.",

+ 12 - 0
src/i18n/locales/hi/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": ".roomodes फ़ाइल में लाइन {{line}} पर अमान्य YAML। कृपया जांचें:\n• सही इंडेंटेशन (टैब नहीं, स्पेस का उपयोग करें)\n• मैचिंग कोट्स और ब्रैकेट्स\n• वैध YAML सिंटैक्स",
+			"schemaValidationError": ".roomodes में अमान्य कस्टम मोड फॉर्मेट:\n{{issues}}",
+			"invalidFormat": "अमान्य कस्टम मोड फॉर्मेट। कृपया सुनिश्चित करें कि आपकी सेटिंग्स सही YAML फॉर्मेट का पालन करती हैं।",
+			"updateFailed": "कस्टम मोड अपडेट विफल: {{error}}",
+			"deleteFailed": "कस्टम मोड डिलीट विफल: {{error}}",
+			"resetFailed": "कस्टम मोड रीसेट विफल: {{error}}",
+			"modeNotFound": "लेखन त्रुटि: मोड नहीं मिला",
+			"noWorkspaceForProject": "प्रोजेक्ट-विशिष्ट मोड के लिए वर्कस्पेस फ़ोल्डर नहीं मिला"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "आपके संगठन को Roo Code Cloud प्रमाणीकरण की आवश्यकता है। कृपया जारी रखने के लिए साइन इन करें।",

+ 12 - 0
src/i18n/locales/id/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "YAML tidak valid dalam file .roomodes pada baris {{line}}. Silakan periksa:\n• Indentasi yang benar (gunakan spasi, bukan tab)\n• Tanda kutip dan kurung yang cocok\n• Sintaks YAML yang valid",
+			"schemaValidationError": "Format mode kustom tidak valid dalam .roomodes:\n{{issues}}",
+			"invalidFormat": "Format mode kustom tidak valid. Pastikan pengaturan kamu mengikuti format YAML yang benar.",
+			"updateFailed": "Gagal memperbarui mode kustom: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Organisasi kamu memerlukan autentikasi Roo Code Cloud. Silakan masuk untuk melanjutkan.",

+ 12 - 0
src/i18n/locales/it/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "YAML non valido nel file .roomodes alla riga {{line}}. Controlla:\n• Indentazione corretta (usa spazi, non tab)\n• Virgolette e parentesi corrispondenti\n• Sintassi YAML valida",
+			"schemaValidationError": "Formato modalità personalizzate non valido in .roomodes:\n{{issues}}",
+			"invalidFormat": "Formato modalità personalizzate non valido. Assicurati che le tue impostazioni seguano il formato YAML corretto.",
+			"updateFailed": "Aggiornamento modalità personalizzata fallito: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "La tua organizzazione richiede l'autenticazione Roo Code Cloud. Accedi per continuare.",

+ 12 - 0
src/i18n/locales/ja/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": ".roomodes ファイルの {{line}} 行目で無効な YAML です。以下を確認してください:\n• 正しいインデント(タブではなくスペースを使用)\n• 引用符と括弧の対応\n• 有効な YAML 構文",
+			"schemaValidationError": ".roomodes のカスタムモード形式が無効です:\n{{issues}}",
+			"invalidFormat": "カスタムモード形式が無効です。設定が正しい YAML 形式に従っていることを確認してください。",
+			"updateFailed": "カスタムモードの更新に失敗しました:{{error}}",
+			"deleteFailed": "カスタムモードの削除に失敗しました:{{error}}",
+			"resetFailed": "カスタムモードのリセットに失敗しました:{{error}}",
+			"modeNotFound": "書き込みエラー:モードが見つかりません",
+			"noWorkspaceForProject": "プロジェクト固有モード用のワークスペースフォルダーが見つかりません"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "あなたの組織では Roo Code Cloud 認証が必要です。続行するにはサインインしてください。",

+ 12 - 0
src/i18n/locales/ko/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": ".roomodes 파일의 {{line}}번째 줄에서 유효하지 않은 YAML입니다. 다음을 확인하세요:\n• 올바른 들여쓰기 (탭이 아닌 공백 사용)\n• 일치하는 따옴표와 괄호\n• 유효한 YAML 구문",
+			"schemaValidationError": ".roomodes의 사용자 정의 모드 형식이 유효하지 않습니다:\n{{issues}}",
+			"invalidFormat": "사용자 정의 모드 형식이 유효하지 않습니다. 설정이 올바른 YAML 형식을 따르는지 확인하세요.",
+			"updateFailed": "사용자 정의 모드 업데이트 실패: {{error}}",
+			"deleteFailed": "사용자 정의 모드 삭제 실패: {{error}}",
+			"resetFailed": "사용자 정의 모드 재설정 실패: {{error}}",
+			"modeNotFound": "쓰기 오류: 모드를 찾을 수 없습니다",
+			"noWorkspaceForProject": "프로젝트별 모드용 작업 공간 폴더를 찾을 수 없습니다"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "조직에서 Roo Code Cloud 인증이 필요합니다. 계속하려면 로그인하세요.",

+ 12 - 0
src/i18n/locales/nl/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "Ongeldige YAML in .roomodes bestand op regel {{line}}. Controleer:\n• Juiste inspringing (gebruik spaties, geen tabs)\n• Overeenkomende aanhalingstekens en haakjes\n• Geldige YAML syntaxis",
+			"schemaValidationError": "Ongeldig aangepaste modi formaat in .roomodes:\n{{issues}}",
+			"invalidFormat": "Ongeldig aangepaste modi formaat. Zorg ervoor dat je instellingen het juiste YAML formaat volgen.",
+			"updateFailed": "Aangepaste modus bijwerken mislukt: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Je organisatie vereist Roo Code Cloud-authenticatie. Log in om door te gaan.",

+ 12 - 0
src/i18n/locales/pl/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "Nieprawidłowy YAML w pliku .roomodes w linii {{line}}. Sprawdź:\n• Prawidłowe wcięcia (używaj spacji, nie tabulatorów)\n• Pasujące cudzysłowy i nawiasy\n• Prawidłową składnię YAML",
+			"schemaValidationError": "Nieprawidłowy format trybów niestandardowych w .roomodes:\n{{issues}}",
+			"invalidFormat": "Nieprawidłowy format trybów niestandardowych. Upewnij się, że twoje ustawienia są zgodne z prawidłowym formatem YAML.",
+			"updateFailed": "Aktualizacja trybu niestandardowego nie powiodła się: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Twoja organizacja wymaga uwierzytelnienia Roo Code Cloud. Zaloguj się, aby kontynuować.",

+ 12 - 0
src/i18n/locales/pt-BR/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "YAML inválido no arquivo .roomodes na linha {{line}}. Verifique:\n• Indentação correta (use espaços, não tabs)\n• Aspas e colchetes correspondentes\n• Sintaxe YAML válida",
+			"schemaValidationError": "Formato de modos personalizados inválido em .roomodes:\n{{issues}}",
+			"invalidFormat": "Formato de modos personalizados inválido. Certifique-se de que suas configurações seguem o formato YAML correto.",
+			"updateFailed": "Falha ao atualizar modo personalizado: {{error}}",
+			"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"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Sua organização requer autenticação do Roo Code Cloud. Faça login para continuar.",

+ 12 - 0
src/i18n/locales/ru/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "Недопустимый YAML в файле .roomodes на строке {{line}}. Проверь:\n• Правильные отступы (используй пробелы, не табы)\n• Соответствующие кавычки и скобки\n• Допустимый синтаксис YAML",
+			"schemaValidationError": "Недопустимый формат пользовательских режимов в .roomodes:\n{{issues}}",
+			"invalidFormat": "Недопустимый формат пользовательских режимов. Убедись, что твои настройки соответствуют правильному формату YAML.",
+			"updateFailed": "Не удалось обновить пользовательский режим: {{error}}",
+			"deleteFailed": "Не удалось удалить пользовательский режим: {{error}}",
+			"resetFailed": "Не удалось сбросить пользовательские режимы: {{error}}",
+			"modeNotFound": "Ошибка записи: Режим не найден",
+			"noWorkspaceForProject": "Не найдена папка рабочего пространства для режима, специфичного для проекта"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Ваша организация требует аутентификации Roo Code Cloud. Войдите в систему, чтобы продолжить.",

+ 12 - 0
src/i18n/locales/tr/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": ".roomodes dosyasının {{line}}. satırında geçersiz YAML. Kontrol et:\n• Doğru girinti (tab değil boşluk kullan)\n• Eşleşen tırnak işaretleri ve parantezler\n• Geçerli YAML sözdizimi",
+			"schemaValidationError": ".roomodes'ta geçersiz özel mod formatı:\n{{issues}}",
+			"invalidFormat": "Geçersiz özel mod formatı. Ayarlarının doğru YAML formatını takip ettiğinden emin ol.",
+			"updateFailed": "Özel mod güncellemesi başarısız: {{error}}",
+			"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ı"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "Kuruluşunuz Roo Code Cloud kimlik doğrulaması gerektiriyor. Devam etmek için giriş yapın.",

+ 12 - 0
src/i18n/locales/vi/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": "YAML không hợp lệ trong tệp .roomodes tại dòng {{line}}. Vui lòng kiểm tra:\n• Thụt lề đúng (dùng dấu cách, không dùng tab)\n• Dấu ngoặc kép và ngoặc đơn khớp nhau\n• Cú pháp YAML hợp lệ",
+			"schemaValidationError": "Định dạng chế độ tùy chỉnh không hợp lệ trong .roomodes:\n{{issues}}",
+			"invalidFormat": "Định dạng chế độ tùy chỉnh không hợp lệ. Vui lòng đảm bảo cài đặt của bạn tuân theo định dạng YAML đúng.",
+			"updateFailed": "Cập nhật chế độ tùy chỉnh thất bại: {{error}}",
+			"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"
+		}
+	},
 	"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.",

+ 12 - 0
src/i18n/locales/zh-CN/common.json

@@ -127,6 +127,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": ".roomodes 文件第 {{line}} 行 YAML 格式无效。请检查:\n• 正确的缩进(使用空格,不要使用制表符)\n• 匹配的引号和括号\n• 有效的 YAML 语法",
+			"schemaValidationError": ".roomodes 中自定义模式格式无效:\n{{issues}}",
+			"invalidFormat": "自定义模式格式无效。请确保你的设置遵循正确的 YAML 格式。",
+			"updateFailed": "更新自定义模式失败:{{error}}",
+			"deleteFailed": "删除自定义模式失败:{{error}}",
+			"resetFailed": "重置自定义模式失败:{{error}}",
+			"modeNotFound": "写入错误:未找到模式",
+			"noWorkspaceForProject": "未找到项目特定模式的工作区文件夹"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "您的组织需要 Roo Code Cloud 身份验证。请登录以继续。",

+ 12 - 0
src/i18n/locales/zh-TW/common.json

@@ -122,6 +122,18 @@
 			}
 		}
 	},
+	"customModes": {
+		"errors": {
+			"yamlParseError": ".roomodes 檔案第 {{line}} 行 YAML 格式無效。請檢查:\n• 正確的縮排(使用空格,不要使用定位字元)\n• 匹配的引號和括號\n• 有效的 YAML 語法",
+			"schemaValidationError": ".roomodes 中自訂模式格式無效:\n{{issues}}",
+			"invalidFormat": "自訂模式格式無效。請確保你的設定遵循正確的 YAML 格式。",
+			"updateFailed": "更新自訂模式失敗:{{error}}",
+			"deleteFailed": "刪除自訂模式失敗:{{error}}",
+			"resetFailed": "重設自訂模式失敗:{{error}}",
+			"modeNotFound": "寫入錯誤:未找到模式",
+			"noWorkspaceForProject": "未找到專案特定模式的工作區資料夾"
+		}
+	},
 	"mdm": {
 		"errors": {
 			"cloud_auth_required": "您的組織需要 Roo Code Cloud 身份驗證。請登入以繼續。",

+ 6 - 2
src/services/marketplace/SimpleInstaller.ts

@@ -84,7 +84,7 @@ export class SimpleInstaller {
 
 		// Write back to file
 		await fs.mkdir(path.dirname(filePath), { recursive: true })
-		const yamlContent = yaml.stringify(existingData)
+		const yamlContent = yaml.stringify(existingData, { lineWidth: 0, defaultStringType: "PLAIN" })
 		await fs.writeFile(filePath, yamlContent, "utf-8")
 
 		// Calculate approximate line number where the new mode was added
@@ -282,7 +282,11 @@ export class SimpleInstaller {
 				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), "utf-8")
+				await fs.writeFile(
+					filePath,
+					yaml.stringify(existingData, { lineWidth: 0, defaultStringType: "PLAIN" }),
+					"utf-8",
+				)
 			}
 		} catch (error: any) {
 			if (error.code === "ENOENT") {

+ 2 - 2
src/utils/migrateSettings.ts

@@ -92,8 +92,8 @@ async function migrateCustomModesToYaml(settingsDir: string, outputChannel: vsco
 			// Parse JSON to object (using the yaml library just to be safe/consistent)
 			const customModesData = yaml.parse(jsonContent)
 
-			// Convert to YAML
-			const yamlContent = yaml.stringify(customModesData)
+			// Convert to YAML with no line width limit to prevent line breaks
+			const yamlContent = yaml.stringify(customModesData, { lineWidth: 0, defaultStringType: "PLAIN" })
 
 			// Write YAML file
 			await fs.writeFile(newYamlPath, yamlContent, "utf-8")