Browse Source

Clean up the settings page

Matt Rubens 11 months ago
parent
commit
993ebaf999

+ 24 - 12
src/core/config/ConfigManager.ts

@@ -64,7 +64,7 @@ export class ConfigManager {
 	/**
 	 * List all available configs with metadata
 	 */
-	async ListConfig(): Promise<ApiConfigMeta[]> {
+	async listConfig(): Promise<ApiConfigMeta[]> {
 		try {
 			const config = await this.readConfig()
 			return Object.entries(config.apiConfigs).map(([name, apiConfig]) => ({
@@ -80,7 +80,7 @@ export class ConfigManager {
 	/**
 	 * Save a config with the given name
 	 */
-	async SaveConfig(name: string, config: ApiConfiguration): Promise<void> {
+	async saveConfig(name: string, config: ApiConfiguration): Promise<void> {
 		try {
 			const currentConfig = await this.readConfig()
 			const existingConfig = currentConfig.apiConfigs[name]
@@ -97,7 +97,7 @@ export class ConfigManager {
 	/**
 	 * Load a config by name
 	 */
-	async LoadConfig(name: string): Promise<ApiConfiguration> {
+	async loadConfig(name: string): Promise<ApiConfiguration> {
 		try {
 			const config = await this.readConfig()
 			const apiConfig = config.apiConfigs[name]
@@ -118,7 +118,7 @@ export class ConfigManager {
 	/**
 	 * Delete a config by name
 	 */
-	async DeleteConfig(name: string): Promise<void> {
+	async deleteConfig(name: string): Promise<void> {
 		try {
 			const currentConfig = await this.readConfig()
 			if (!currentConfig.apiConfigs[name]) {
@@ -140,7 +140,7 @@ export class ConfigManager {
 	/**
 	 * Set the current active API configuration
 	 */
-	async SetCurrentConfig(name: string): Promise<void> {
+	async setCurrentConfig(name: string): Promise<void> {
 		try {
 			const currentConfig = await this.readConfig()
 			if (!currentConfig.apiConfigs[name]) {
@@ -157,7 +157,7 @@ export class ConfigManager {
 	/**
 	 * Check if a config exists by name
 	 */
-	async HasConfig(name: string): Promise<boolean> {
+	async hasConfig(name: string): Promise<boolean> {
 		try {
 			const config = await this.readConfig()
 			return name in config.apiConfigs
@@ -169,7 +169,7 @@ export class ConfigManager {
 	/**
 	 * Set the API config for a specific mode
 	 */
-	async SetModeConfig(mode: Mode, configId: string): Promise<void> {
+	async setModeConfig(mode: Mode, configId: string): Promise<void> {
 		try {
 			const currentConfig = await this.readConfig()
 			if (!currentConfig.modeApiConfigs) {
@@ -185,7 +185,7 @@ export class ConfigManager {
 	/**
 	 * Get the API config ID for a specific mode
 	 */
-	async GetModeConfigId(mode: Mode): Promise<string | undefined> {
+	async getModeConfigId(mode: Mode): Promise<string | undefined> {
 		try {
 			const config = await this.readConfig()
 			return config.modeApiConfigs?.[mode]
@@ -194,10 +194,23 @@ export class ConfigManager {
 		}
 	}
 
+	/**
+	 * Get the key used for storing config in secrets
+	 */
+	private getConfigKey(): string {
+		return `${this.SCOPE_PREFIX}api_config`
+	}
+
+	/**
+	 * Reset all configuration by deleting the stored config from secrets
+	 */
+	public async resetAllConfigs(): Promise<void> {
+		await this.context.secrets.delete(this.getConfigKey())
+	}
+
 	private async readConfig(): Promise<ApiConfigData> {
 		try {
-			const configKey = `${this.SCOPE_PREFIX}api_config`
-			const content = await this.context.secrets.get(configKey)
+			const content = await this.context.secrets.get(this.getConfigKey())
 
 			if (!content) {
 				return this.defaultConfig
@@ -211,9 +224,8 @@ export class ConfigManager {
 
 	private async writeConfig(config: ApiConfigData): Promise<void> {
 		try {
-			const configKey = `${this.SCOPE_PREFIX}api_config`
 			const content = JSON.stringify(config, null, 2)
-			await this.context.secrets.store(configKey, content)
+			await this.context.secrets.store(this.getConfigKey(), content)
 		} catch (error) {
 			throw new Error(`Failed to write config to secrets: ${error}`)
 		}

+ 40 - 18
src/core/config/__tests__/ConfigManager.test.ts

@@ -106,7 +106,7 @@ describe("ConfigManager", () => {
 
 			mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
 
-			const configs = await configManager.ListConfig()
+			const configs = await configManager.listConfig()
 			expect(configs).toEqual([
 				{ name: "default", id: "default", apiProvider: undefined },
 				{ name: "test", id: "test-id", apiProvider: "anthropic" },
@@ -126,14 +126,14 @@ describe("ConfigManager", () => {
 
 			mockSecrets.get.mockResolvedValue(JSON.stringify(emptyConfig))
 
-			const configs = await configManager.ListConfig()
+			const configs = await configManager.listConfig()
 			expect(configs).toEqual([])
 		})
 
 		it("should throw error if reading from secrets fails", async () => {
 			mockSecrets.get.mockRejectedValue(new Error("Read failed"))
 
-			await expect(configManager.ListConfig()).rejects.toThrow(
+			await expect(configManager.listConfig()).rejects.toThrow(
 				"Failed to list configs: Error: Failed to read config from secrets: Error: Read failed",
 			)
 		})
@@ -160,7 +160,7 @@ describe("ConfigManager", () => {
 				apiKey: "test-key",
 			}
 
-			await configManager.SaveConfig("test", newConfig)
+			await configManager.saveConfig("test", newConfig)
 
 			// Get the actual stored config to check the generated ID
 			const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
@@ -207,7 +207,7 @@ describe("ConfigManager", () => {
 				apiKey: "new-key",
 			}
 
-			await configManager.SaveConfig("test", updatedConfig)
+			await configManager.saveConfig("test", updatedConfig)
 
 			const expectedConfig = {
 				currentApiConfigName: "default",
@@ -235,7 +235,7 @@ describe("ConfigManager", () => {
 			)
 			mockSecrets.store.mockRejectedValueOnce(new Error("Storage failed"))
 
-			await expect(configManager.SaveConfig("test", {})).rejects.toThrow(
+			await expect(configManager.saveConfig("test", {})).rejects.toThrow(
 				"Failed to save config: Error: Failed to write config to secrets: Error: Storage failed",
 			)
 		})
@@ -258,7 +258,7 @@ describe("ConfigManager", () => {
 
 			mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
 
-			await configManager.DeleteConfig("test")
+			await configManager.deleteConfig("test")
 
 			// Get the stored config to check the ID
 			const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
@@ -275,7 +275,7 @@ describe("ConfigManager", () => {
 				}),
 			)
 
-			await expect(configManager.DeleteConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
+			await expect(configManager.deleteConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
 		})
 
 		it("should throw error when trying to delete last remaining config", async () => {
@@ -290,7 +290,7 @@ describe("ConfigManager", () => {
 				}),
 			)
 
-			await expect(configManager.DeleteConfig("default")).rejects.toThrow(
+			await expect(configManager.deleteConfig("default")).rejects.toThrow(
 				"Cannot delete the last remaining configuration.",
 			)
 		})
@@ -311,7 +311,7 @@ describe("ConfigManager", () => {
 
 			mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
 
-			const config = await configManager.LoadConfig("test")
+			const config = await configManager.loadConfig("test")
 
 			expect(config).toEqual({
 				apiProvider: "anthropic",
@@ -342,7 +342,7 @@ describe("ConfigManager", () => {
 				}),
 			)
 
-			await expect(configManager.LoadConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
+			await expect(configManager.loadConfig("nonexistent")).rejects.toThrow("Config 'nonexistent' not found")
 		})
 
 		it("should throw error if secrets storage fails", async () => {
@@ -361,7 +361,7 @@ describe("ConfigManager", () => {
 			)
 			mockSecrets.store.mockRejectedValueOnce(new Error("Storage failed"))
 
-			await expect(configManager.LoadConfig("test")).rejects.toThrow(
+			await expect(configManager.loadConfig("test")).rejects.toThrow(
 				"Failed to load config: Error: Failed to write config to secrets: Error: Storage failed",
 			)
 		})
@@ -384,7 +384,7 @@ describe("ConfigManager", () => {
 
 			mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
 
-			await configManager.SetCurrentConfig("test")
+			await configManager.setCurrentConfig("test")
 
 			// Get the stored config to check the structure
 			const storedConfig = JSON.parse(mockSecrets.store.mock.calls[0][1])
@@ -404,7 +404,7 @@ describe("ConfigManager", () => {
 				}),
 			)
 
-			await expect(configManager.SetCurrentConfig("nonexistent")).rejects.toThrow(
+			await expect(configManager.setCurrentConfig("nonexistent")).rejects.toThrow(
 				"Config 'nonexistent' not found",
 			)
 		})
@@ -420,12 +420,34 @@ describe("ConfigManager", () => {
 			)
 			mockSecrets.store.mockRejectedValueOnce(new Error("Storage failed"))
 
-			await expect(configManager.SetCurrentConfig("test")).rejects.toThrow(
+			await expect(configManager.setCurrentConfig("test")).rejects.toThrow(
 				"Failed to set current config: Error: Failed to write config to secrets: Error: Storage failed",
 			)
 		})
 	})
 
+	describe("ResetAllConfigs", () => {
+		it("should delete all stored configs", async () => {
+			// Setup initial config
+			mockSecrets.get.mockResolvedValue(
+				JSON.stringify({
+					currentApiConfigName: "test",
+					apiConfigs: {
+						test: {
+							apiProvider: "anthropic",
+							id: "test-id",
+						},
+					},
+				}),
+			)
+
+			await configManager.resetAllConfigs()
+
+			// Should have called delete with the correct config key
+			expect(mockSecrets.delete).toHaveBeenCalledWith("roo_cline_config_api_config")
+		})
+	})
+
 	describe("HasConfig", () => {
 		it("should return true for existing config", async () => {
 			const existingConfig: ApiConfigData = {
@@ -443,7 +465,7 @@ describe("ConfigManager", () => {
 
 			mockSecrets.get.mockResolvedValue(JSON.stringify(existingConfig))
 
-			const hasConfig = await configManager.HasConfig("test")
+			const hasConfig = await configManager.hasConfig("test")
 			expect(hasConfig).toBe(true)
 		})
 
@@ -455,14 +477,14 @@ describe("ConfigManager", () => {
 				}),
 			)
 
-			const hasConfig = await configManager.HasConfig("nonexistent")
+			const hasConfig = await configManager.hasConfig("nonexistent")
 			expect(hasConfig).toBe(false)
 		})
 
 		it("should throw error if secrets storage fails", async () => {
 			mockSecrets.get.mockRejectedValue(new Error("Storage failed"))
 
-			await expect(configManager.HasConfig("test")).rejects.toThrow(
+			await expect(configManager.hasConfig("test")).rejects.toThrow(
 				"Failed to check config existence: Error: Failed to read config from secrets: Error: Storage failed",
 			)
 		})

+ 33 - 24
src/core/webview/ClineProvider.ts

@@ -446,7 +446,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						})
 
 						this.configManager
-							.ListConfig()
+							.listConfig()
 							.then(async (listApiConfig) => {
 								if (!listApiConfig) {
 									return
@@ -456,7 +456,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 									// check if first time init then sync with exist config
 									if (!checkExistKey(listApiConfig[0])) {
 										const { apiConfiguration } = await this.getState()
-										await this.configManager.SaveConfig(
+										await this.configManager.saveConfig(
 											listApiConfig[0].name ?? "default",
 											apiConfiguration,
 										)
@@ -467,11 +467,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 								let currentConfigName = (await this.getGlobalState("currentApiConfigName")) as string
 
 								if (currentConfigName) {
-									if (!(await this.configManager.HasConfig(currentConfigName))) {
+									if (!(await this.configManager.hasConfig(currentConfigName))) {
 										// current config name not valid, get first config in list
 										await this.updateGlobalState("currentApiConfigName", listApiConfig?.[0]?.name)
 										if (listApiConfig?.[0]?.name) {
-											const apiConfig = await this.configManager.LoadConfig(
+											const apiConfig = await this.configManager.loadConfig(
 												listApiConfig?.[0]?.name,
 											)
 
@@ -726,8 +726,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("mode", newMode)
 
 						// Load the saved API config for the new mode if it exists
-						const savedConfigId = await this.configManager.GetModeConfigId(newMode)
-						const listApiConfig = await this.configManager.ListConfig()
+						const savedConfigId = await this.configManager.getModeConfigId(newMode)
+						const listApiConfig = await this.configManager.listConfig()
 
 						// Update listApiConfigMeta first to ensure UI has latest data
 						await this.updateGlobalState("listApiConfigMeta", listApiConfig)
@@ -736,7 +736,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						if (savedConfigId) {
 							const config = listApiConfig?.find((c) => c.id === savedConfigId)
 							if (config?.name) {
-								const apiConfig = await this.configManager.LoadConfig(config.name)
+								const apiConfig = await this.configManager.loadConfig(config.name)
 								await Promise.all([
 									this.updateGlobalState("currentApiConfigName", config.name),
 									this.updateApiConfiguration(apiConfig),
@@ -748,7 +748,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 							if (currentApiConfigName) {
 								const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
 								if (config?.id) {
-									await this.configManager.SetModeConfig(newMode, config.id)
+									await this.configManager.setModeConfig(newMode, config.id)
 								}
 							}
 						}
@@ -913,7 +913,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 								if (enhancementApiConfigId) {
 									const config = listApiConfigMeta?.find((c) => c.id === enhancementApiConfigId)
 									if (config?.name) {
-										const loadedConfig = await this.configManager.LoadConfig(config.name)
+										const loadedConfig = await this.configManager.loadConfig(config.name)
 										if (loadedConfig.apiProvider) {
 											configToUse = loadedConfig
 										}
@@ -1004,8 +1004,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 					case "upsertApiConfiguration":
 						if (message.text && message.apiConfiguration) {
 							try {
-								await this.configManager.SaveConfig(message.text, message.apiConfiguration)
-								let listApiConfig = await this.configManager.ListConfig()
+								await this.configManager.saveConfig(message.text, message.apiConfiguration)
+								let listApiConfig = await this.configManager.listConfig()
 
 								await Promise.all([
 									this.updateGlobalState("listApiConfigMeta", listApiConfig),
@@ -1025,10 +1025,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 							try {
 								const { oldName, newName } = message.values
 
-								await this.configManager.SaveConfig(newName, message.apiConfiguration)
-								await this.configManager.DeleteConfig(oldName)
+								await this.configManager.saveConfig(newName, message.apiConfiguration)
+								await this.configManager.deleteConfig(oldName)
 
-								let listApiConfig = await this.configManager.ListConfig()
+								let listApiConfig = await this.configManager.listConfig()
 								const config = listApiConfig?.find((c) => c.name === newName)
 
 								// Update listApiConfigMeta first to ensure UI has latest data
@@ -1046,8 +1046,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 					case "loadApiConfiguration":
 						if (message.text) {
 							try {
-								const apiConfig = await this.configManager.LoadConfig(message.text)
-								const listApiConfig = await this.configManager.ListConfig()
+								const apiConfig = await this.configManager.loadConfig(message.text)
+								const listApiConfig = await this.configManager.listConfig()
 
 								await Promise.all([
 									this.updateGlobalState("listApiConfigMeta", listApiConfig),
@@ -1075,8 +1075,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 							}
 
 							try {
-								await this.configManager.DeleteConfig(message.text)
-								const listApiConfig = await this.configManager.ListConfig()
+								await this.configManager.deleteConfig(message.text)
+								const listApiConfig = await this.configManager.listConfig()
 
 								// Update listApiConfigMeta first to ensure UI has latest data
 								await this.updateGlobalState("listApiConfigMeta", listApiConfig)
@@ -1084,7 +1084,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 								// If this was the current config, switch to first available
 								let currentApiConfigName = await this.getGlobalState("currentApiConfigName")
 								if (message.text === currentApiConfigName && listApiConfig?.[0]?.name) {
-									const apiConfig = await this.configManager.LoadConfig(listApiConfig[0].name)
+									const apiConfig = await this.configManager.loadConfig(listApiConfig[0].name)
 									await Promise.all([
 										this.updateGlobalState("currentApiConfigName", listApiConfig[0].name),
 										this.updateApiConfiguration(apiConfig),
@@ -1100,7 +1100,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						break
 					case "getListApiConfiguration":
 						try {
-							let listApiConfig = await this.configManager.ListConfig()
+							let listApiConfig = await this.configManager.listConfig()
 							await this.updateGlobalState("listApiConfigMeta", listApiConfig)
 							this.postMessageToWebview({ type: "listApiConfig", listApiConfig })
 						} catch (error) {
@@ -1127,10 +1127,10 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const { mode } = await this.getState()
 		if (mode) {
 			const currentApiConfigName = await this.getGlobalState("currentApiConfigName")
-			const listApiConfig = await this.configManager.ListConfig()
+			const listApiConfig = await this.configManager.listConfig()
 			const config = listApiConfig?.find((c) => c.name === currentApiConfigName)
 			if (config?.id) {
-				await this.configManager.SetModeConfig(mode, config.id)
+				await this.configManager.setModeConfig(mode, config.id)
 			}
 		}
 
@@ -2077,7 +2077,16 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	// dev
 
 	async resetState() {
-		vscode.window.showInformationMessage("Resetting state...")
+		const answer = await vscode.window.showInformationMessage(
+			"Are you sure you want to reset all state and secret storage in the extension? This cannot be undone.",
+			{ modal: true },
+			"Yes",
+		)
+
+		if (answer !== "Yes") {
+			return
+		}
+
 		for (const key of this.context.globalState.keys()) {
 			await this.context.globalState.update(key, undefined)
 		}
@@ -2097,11 +2106,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		for (const key of secretKeys) {
 			await this.storeSecret(key, undefined)
 		}
+		await this.configManager.resetAllConfigs()
 		if (this.cline) {
 			this.cline.abortTask()
 			this.cline = undefined
 		}
-		vscode.window.showInformationMessage("State reset")
 		await this.postStateToWebview()
 		await this.postMessageToWebview({ type: "action", action: "chatButtonClicked" })
 	}

+ 18 - 18
src/core/webview/__tests__/ClineProvider.test.ts

@@ -443,18 +443,18 @@ describe("ClineProvider", () => {
 
 		// Mock ConfigManager methods
 		provider.configManager = {
-			GetModeConfigId: jest.fn().mockResolvedValue("test-id"),
-			ListConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
-			LoadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic" }),
-			SetModeConfig: jest.fn(),
+			getModeConfigId: jest.fn().mockResolvedValue("test-id"),
+			listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
+			loadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic" }),
+			setModeConfig: jest.fn(),
 		} as any
 
 		// Switch to architect mode
 		await messageHandler({ type: "mode", text: "architect" })
 
 		// Should load the saved config for architect mode
-		expect(provider.configManager.GetModeConfigId).toHaveBeenCalledWith("architect")
-		expect(provider.configManager.LoadConfig).toHaveBeenCalledWith("test-config")
+		expect(provider.configManager.getModeConfigId).toHaveBeenCalledWith("architect")
+		expect(provider.configManager.loadConfig).toHaveBeenCalledWith("test-config")
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("currentApiConfigName", "test-config")
 	})
 
@@ -464,11 +464,11 @@ describe("ClineProvider", () => {
 
 		// Mock ConfigManager methods
 		provider.configManager = {
-			GetModeConfigId: jest.fn().mockResolvedValue(undefined),
-			ListConfig: jest
+			getModeConfigId: jest.fn().mockResolvedValue(undefined),
+			listConfig: jest
 				.fn()
 				.mockResolvedValue([{ name: "current-config", id: "current-id", apiProvider: "anthropic" }]),
-			SetModeConfig: jest.fn(),
+			setModeConfig: jest.fn(),
 		} as any
 
 		// Mock current config name
@@ -483,7 +483,7 @@ describe("ClineProvider", () => {
 		await messageHandler({ type: "mode", text: "architect" })
 
 		// Should save current config as default for architect mode
-		expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith("architect", "current-id")
+		expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("architect", "current-id")
 	})
 
 	test("saves config as default for current mode when loading config", async () => {
@@ -491,10 +491,10 @@ describe("ClineProvider", () => {
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		provider.configManager = {
-			LoadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic", id: "new-id" }),
-			ListConfig: jest.fn().mockResolvedValue([{ name: "new-config", id: "new-id", apiProvider: "anthropic" }]),
-			SetModeConfig: jest.fn(),
-			GetModeConfigId: jest.fn().mockResolvedValue(undefined),
+			loadConfig: jest.fn().mockResolvedValue({ apiProvider: "anthropic", id: "new-id" }),
+			listConfig: jest.fn().mockResolvedValue([{ name: "new-config", id: "new-id", apiProvider: "anthropic" }]),
+			setModeConfig: jest.fn(),
+			getModeConfigId: jest.fn().mockResolvedValue(undefined),
 		} as any
 
 		// First set the mode
@@ -504,7 +504,7 @@ describe("ClineProvider", () => {
 		await messageHandler({ type: "loadApiConfiguration", text: "new-config" })
 
 		// Should save new config as default for architect mode
-		expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith("architect", "new-id")
+		expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("architect", "new-id")
 	})
 
 	test("handles request delay settings messages", async () => {
@@ -678,8 +678,8 @@ describe("ClineProvider", () => {
 		const messageHandler = (mockWebviewView.webview.onDidReceiveMessage as jest.Mock).mock.calls[0][0]
 
 		provider.configManager = {
-			ListConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
-			SetModeConfig: jest.fn(),
+			listConfig: jest.fn().mockResolvedValue([{ name: "test-config", id: "test-id", apiProvider: "anthropic" }]),
+			setModeConfig: jest.fn(),
 		} as any
 
 		// Update API configuration
@@ -689,7 +689,7 @@ describe("ClineProvider", () => {
 		})
 
 		// Should save config as default for current mode
-		expect(provider.configManager.SetModeConfig).toHaveBeenCalledWith("code", "test-id")
+		expect(provider.configManager.setModeConfig).toHaveBeenCalledWith("code", "test-id")
 	})
 
 	test("file content includes line numbers", async () => {

+ 51 - 0
webview-ui/src/components/prompts/PromptsView.tsx

@@ -22,6 +22,8 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 		mode,
 		customInstructions,
 		setCustomInstructions,
+		preferredLanguage,
+		setPreferredLanguage,
 	} = useExtensionState()
 	const [testPrompt, setTestPrompt] = useState("")
 	const [isEnhancing, setIsEnhancing] = useState(false)
@@ -146,6 +148,55 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 
 			<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
 				<div style={{ marginBottom: "20px" }}>
+					<div style={{ marginBottom: "20px" }}>
+						<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Preferred Language</div>
+						<select
+							value={preferredLanguage}
+							onChange={(e) => {
+								setPreferredLanguage(e.target.value)
+								vscode.postMessage({
+									type: "preferredLanguage",
+									text: e.target.value,
+								})
+							}}
+							style={{
+								width: "100%",
+								padding: "4px 8px",
+								backgroundColor: "var(--vscode-input-background)",
+								color: "var(--vscode-input-foreground)",
+								border: "1px solid var(--vscode-input-border)",
+								borderRadius: "2px",
+								height: "28px",
+							}}>
+							<option value="English">English</option>
+							<option value="Arabic">Arabic - العربية</option>
+							<option value="Brazilian Portuguese">Portuguese - Português (Brasil)</option>
+							<option value="Czech">Czech - Čeština</option>
+							<option value="French">French - Français</option>
+							<option value="German">German - Deutsch</option>
+							<option value="Hindi">Hindi - हिन्दी</option>
+							<option value="Hungarian">Hungarian - Magyar</option>
+							<option value="Italian">Italian - Italiano</option>
+							<option value="Japanese">Japanese - 日本語</option>
+							<option value="Korean">Korean - 한국어</option>
+							<option value="Polish">Polish - Polski</option>
+							<option value="Portuguese">Portuguese - Português (Portugal)</option>
+							<option value="Russian">Russian - Русский</option>
+							<option value="Simplified Chinese">Simplified Chinese - 简体中文</option>
+							<option value="Spanish">Spanish - Español</option>
+							<option value="Traditional Chinese">Traditional Chinese - 繁體中文</option>
+							<option value="Turkish">Turkish - Türkçe</option>
+						</select>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							Select the language that Cline should use for communication.
+						</p>
+					</div>
+
 					<div style={{ fontWeight: "bold", marginBottom: "4px" }}>Custom Instructions for All Modes</div>
 					<div
 						style={{ fontSize: "13px", color: "var(--vscode-descriptionForeground)", marginBottom: "8px" }}>

+ 251 - 353
webview-ui/src/components/settings/SettingsView.tsx

@@ -1,20 +1,11 @@
-import {
-	VSCodeButton,
-	VSCodeCheckbox,
-	VSCodeLink,
-	VSCodeTextArea,
-	VSCodeTextField,
-} from "@vscode/webview-ui-toolkit/react"
+import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
 import { memo, useEffect, useState } from "react"
 import { useExtensionState } from "../../context/ExtensionStateContext"
 import { validateApiConfiguration, validateModelId } from "../../utils/validate"
 import { vscode } from "../../utils/vscode"
 import ApiOptions from "./ApiOptions"
-import McpEnabledToggle from "../mcp/McpEnabledToggle"
 import ApiConfigManager from "./ApiConfigManager"
 
-const IS_DEV = false // FIXME: use flags when packaging
-
 type SettingsViewProps = {
 	onDone: () => void
 }
@@ -23,8 +14,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 	const {
 		apiConfiguration,
 		version,
-		customInstructions,
-		setCustomInstructions,
 		alwaysAllowReadOnly,
 		setAlwaysAllowReadOnly,
 		alwaysAllowWrite,
@@ -49,8 +38,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 		allowedCommands,
 		fuzzyMatchThreshold,
 		setFuzzyMatchThreshold,
-		preferredLanguage,
-		setPreferredLanguage,
 		writeDelayMs,
 		setWriteDelayMs,
 		screenshotQuality,
@@ -82,7 +69,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 				type: "apiConfiguration",
 				apiConfiguration,
 			})
-			vscode.postMessage({ type: "customInstructions", text: customInstructions })
 			vscode.postMessage({ type: "alwaysAllowReadOnly", bool: alwaysAllowReadOnly })
 			vscode.postMessage({ type: "alwaysAllowWrite", bool: alwaysAllowWrite })
 			vscode.postMessage({ type: "alwaysAllowExecute", bool: alwaysAllowExecute })
@@ -94,7 +80,6 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
 			vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
-			vscode.postMessage({ type: "preferredLanguage", text: preferredLanguage })
 			vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
 			vscode.postMessage({ type: "screenshotQuality", value: screenshotQuality ?? 75 })
 			vscode.postMessage({ type: "terminalOutputLineLimit", value: terminalOutputLineLimit ?? 500 })
@@ -168,257 +153,69 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 			</div>
 			<div
 				style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
-				<div style={{ marginBottom: 5 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
-						Provider Settings
-					</h3>
-					<ApiConfigManager
-						currentApiConfigName={currentApiConfigName}
-						listApiConfigMeta={listApiConfigMeta}
-						onSelectConfig={(configName: string) => {
-							vscode.postMessage({
-								type: "loadApiConfiguration",
-								text: configName,
-							})
-						}}
-						onDeleteConfig={(configName: string) => {
-							vscode.postMessage({
-								type: "deleteApiConfiguration",
-								text: configName,
-							})
-						}}
-						onRenameConfig={(oldName: string, newName: string) => {
-							vscode.postMessage({
-								type: "renameApiConfiguration",
-								values: { oldName, newName },
-								apiConfiguration,
-							})
-						}}
-						onUpsertConfig={(configName: string) => {
-							vscode.postMessage({
-								type: "upsertApiConfiguration",
-								text: configName,
-								apiConfiguration,
-							})
-						}}
-					/>
-					<ApiOptions apiErrorMessage={apiErrorMessage} modelIdErrorMessage={modelIdErrorMessage} />
-				</div>
-
-				<div style={{ marginBottom: 5 }}>
+				<div style={{ marginBottom: 40 }}>
+					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Provider Settings</h3>
 					<div style={{ marginBottom: 15 }}>
-						<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
-							Agent Settings
-						</h3>
-
-						<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
-							Preferred Language
-						</label>
-						<select
-							value={preferredLanguage}
-							onChange={(e) => setPreferredLanguage(e.target.value)}
-							style={{
-								width: "100%",
-								padding: "4px 8px",
-								backgroundColor: "var(--vscode-input-background)",
-								color: "var(--vscode-input-foreground)",
-								border: "1px solid var(--vscode-input-border)",
-								borderRadius: "2px",
-								height: "28px",
-							}}>
-							<option value="English">English</option>
-							<option value="Arabic">Arabic - العربية</option>
-							<option value="Brazilian Portuguese">Portuguese - Português (Brasil)</option>
-							<option value="Czech">Czech - Čeština</option>
-							<option value="French">French - Français</option>
-							<option value="German">German - Deutsch</option>
-							<option value="Hindi">Hindi - हिन्दी</option>
-							<option value="Hungarian">Hungarian - Magyar</option>
-							<option value="Italian">Italian - Italiano</option>
-							<option value="Japanese">Japanese - 日本語</option>
-							<option value="Korean">Korean - 한국어</option>
-							<option value="Polish">Polish - Polski</option>
-							<option value="Portuguese">Portuguese - Português (Portugal)</option>
-							<option value="Russian">Russian - Русский</option>
-							<option value="Simplified Chinese">Simplified Chinese - 简体中文</option>
-							<option value="Spanish">Spanish - Español</option>
-							<option value="Traditional Chinese">Traditional Chinese - 繁體中文</option>
-							<option value="Turkish">Turkish - Türkçe</option>
-						</select>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							Select the language that Cline should use for communication.
-						</p>
+						<ApiConfigManager
+							currentApiConfigName={currentApiConfigName}
+							listApiConfigMeta={listApiConfigMeta}
+							onSelectConfig={(configName: string) => {
+								vscode.postMessage({
+									type: "loadApiConfiguration",
+									text: configName,
+								})
+							}}
+							onDeleteConfig={(configName: string) => {
+								vscode.postMessage({
+									type: "deleteApiConfiguration",
+									text: configName,
+								})
+							}}
+							onRenameConfig={(oldName: string, newName: string) => {
+								vscode.postMessage({
+									type: "renameApiConfiguration",
+									values: { oldName, newName },
+									apiConfiguration,
+								})
+							}}
+							onUpsertConfig={(configName: string) => {
+								vscode.postMessage({
+									type: "upsertApiConfiguration",
+									text: configName,
+									apiConfiguration,
+								})
+							}}
+						/>
+						<ApiOptions apiErrorMessage={apiErrorMessage} modelIdErrorMessage={modelIdErrorMessage} />
 					</div>
+				</div>
+
+				<div style={{ marginBottom: 40 }}>
+					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Auto-Approve Settings</h3>
+					<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
+						The following settings allow Cline to automatically perform operations without requiring
+						approval. Enable these settings only if you fully trust the AI and understand the associated
+						security risks.
+					</p>
 
 					<div style={{ marginBottom: 15 }}>
-						<span style={{ fontWeight: "500" }}>Custom Instructions</span>
-						<VSCodeTextArea
-							value={customInstructions ?? ""}
-							style={{ width: "100%" }}
-							rows={4}
-							placeholder={
-								'e.g. "Run unit tests at the end", "Use TypeScript with async/await", "Speak in Spanish"'
-							}
-							onInput={(e: any) => setCustomInstructions(e.target?.value ?? "")}
-						/>
+						<VSCodeCheckbox
+							checked={alwaysAllowReadOnly}
+							onChange={(e: any) => setAlwaysAllowReadOnly(e.target.checked)}>
+							<span style={{ fontWeight: "500" }}>Always approve read-only operations</span>
+						</VSCodeCheckbox>
 						<p
 							style={{
 								fontSize: "12px",
 								marginTop: "5px",
 								color: "var(--vscode-descriptionForeground)",
 							}}>
-							These instructions are added to the end of the system prompt sent with every request. Custom
-							instructions set in .clinerules in the working directory are also included. For
-							mode-specific instructions, use the{" "}
-							<span className="codicon codicon-notebook" style={{ fontSize: "10px" }}></span> Prompts tab
-							in the top menu.
+							When enabled, Cline will automatically view directory contents and read files without
+							requiring you to click the Approve button.
 						</p>
 					</div>
 
-					<McpEnabledToggle />
-				</div>
-
-				<div style={{ marginBottom: 5 }}>
-					<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-						<span style={{ fontWeight: "500", minWidth: "150px" }}>Terminal output limit</span>
-						<input
-							type="range"
-							min="100"
-							max="5000"
-							step="100"
-							value={terminalOutputLineLimit ?? 500}
-							onChange={(e) => setTerminalOutputLineLimit(parseInt(e.target.value))}
-							style={{
-								flexGrow: 1,
-								accentColor: "var(--vscode-button-background)",
-								height: "2px",
-							}}
-						/>
-						<span style={{ minWidth: "45px", textAlign: "left" }}>{terminalOutputLineLimit ?? 500}</span>
-					</div>
-					<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-						Maximum number of lines to include in terminal output when executing commands. When exceeded
-						lines will be removed from the middle, saving tokens.
-					</p>
-				</div>
-
-				<div style={{ marginBottom: 5 }}>
-					<VSCodeCheckbox
-						checked={diffEnabled}
-						onChange={(e: any) => {
-							setDiffEnabled(e.target.checked)
-							if (!e.target.checked) {
-								// Reset experimental strategy when diffs are disabled
-								setExperimentalDiffStrategy(false)
-							}
-						}}>
-						<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
-					</VSCodeCheckbox>
-					<p
-						style={{
-							fontSize: "12px",
-							marginTop: "5px",
-							color: "var(--vscode-descriptionForeground)",
-						}}>
-						When enabled, Cline will be able to edit files more quickly and will automatically reject
-						truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
-					</p>
-
-					{diffEnabled && (
-						<div style={{ marginTop: 10 }}>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
-								<VSCodeCheckbox
-									checked={experimentalDiffStrategy}
-									onChange={(e: any) => setExperimentalDiffStrategy(e.target.checked)}>
-									<span style={{ fontWeight: "500" }}>Use experimental unified diff strategy</span>
-								</VSCodeCheckbox>
-							</div>
-							<p
-								style={{
-									fontSize: "12px",
-									marginBottom: 15,
-									color: "var(--vscode-descriptionForeground)",
-								}}>
-								Enable the experimental unified diff strategy. This strategy might reduce the number of
-								retries caused by model errors but may cause unexpected behavior or incorrect edits.
-								Only enable if you understand the risks and are willing to carefully review all changes.
-							</p>
-
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<span style={{ fontWeight: "500", minWidth: "100px" }}>Match precision</span>
-								<input
-									type="range"
-									min="0.8"
-									max="1"
-									step="0.005"
-									value={fuzzyMatchThreshold ?? 1.0}
-									onChange={(e) => {
-										setFuzzyMatchThreshold(parseFloat(e.target.value))
-									}}
-									style={{
-										flexGrow: 1,
-										accentColor: "var(--vscode-button-background)",
-										height: "2px",
-									}}
-								/>
-								<span style={{ minWidth: "35px", textAlign: "left" }}>
-									{Math.round((fuzzyMatchThreshold || 1) * 100)}%
-								</span>
-							</div>
-							<p
-								style={{
-									fontSize: "12px",
-									marginTop: "5px",
-									color: "var(--vscode-descriptionForeground)",
-								}}>
-								This slider controls how precisely code sections must match when applying diffs. Lower
-								values allow more flexible matching but increase the risk of incorrect replacements. Use
-								values below 100% with extreme caution.
-							</p>
-						</div>
-					)}
-				</div>
-
-				<div style={{ marginBottom: 5 }}>
-					<VSCodeCheckbox
-						checked={alwaysAllowReadOnly}
-						onChange={(e: any) => setAlwaysAllowReadOnly(e.target.checked)}>
-						<span style={{ fontWeight: "500" }}>Always approve read-only operations</span>
-					</VSCodeCheckbox>
-					<p
-						style={{
-							fontSize: "12px",
-							marginTop: "5px",
-							color: "var(--vscode-descriptionForeground)",
-						}}>
-						When enabled, Cline will automatically view directory contents and read files without requiring
-						you to click the Approve button.
-					</p>
-				</div>
-
-				<div
-					style={{
-						marginBottom: 15,
-						border: "2px solid var(--vscode-errorForeground)",
-						borderRadius: "4px",
-						padding: "10px",
-					}}>
-					<h4 style={{ fontWeight: 500, margin: "0 0 10px 0", color: "var(--vscode-errorForeground)" }}>
-						⚠️ High-Risk Auto-Approve Settings
-					</h4>
-					<p style={{ fontSize: "12px", marginBottom: 15, color: "var(--vscode-descriptionForeground)" }}>
-						The following settings allow Cline to automatically perform potentially dangerous operations
-						without requiring approval. Enable these settings only if you fully trust the AI and understand
-						the associated security risks.
-					</p>
-
-					<div style={{ marginBottom: 5 }}>
+					<div style={{ marginBottom: 15 }}>
 						<VSCodeCheckbox
 							checked={alwaysAllowWrite}
 							onChange={(e: any) => setAlwaysAllowWrite(e.target.checked)}>
@@ -457,7 +254,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 						)}
 					</div>
 
-					<div style={{ marginBottom: 5 }}>
+					<div style={{ marginBottom: 15 }}>
 						<VSCodeCheckbox
 							checked={alwaysAllowBrowser}
 							onChange={(e: any) => setAlwaysAllowBrowser(e.target.checked)}>
@@ -470,7 +267,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 						</p>
 					</div>
 
-					<div style={{ marginBottom: 5 }}>
+					<div style={{ marginBottom: 15 }}>
 						<VSCodeCheckbox
 							checked={alwaysApproveResubmit}
 							onChange={(e: any) => setAlwaysApproveResubmit(e.target.checked)}>
@@ -521,7 +318,7 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 						</p>
 					</div>
 
-					<div style={{ marginBottom: 5 }}>
+					<div style={{ marginBottom: 15 }}>
 						<VSCodeCheckbox
 							checked={alwaysAllowExecute}
 							onChange={(e: any) => setAlwaysAllowExecute(e.target.checked)}>
@@ -614,136 +411,219 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 					</div>
 				</div>
 
-				<div style={{ marginBottom: 5 }}>
-					<div style={{ marginBottom: 10 }}>
-						<div style={{ marginBottom: 15 }}>
-							<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
-								Browser Settings
-							</h3>
-							<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
-								Viewport size
-							</label>
-							<select
-								value={browserViewportSize}
-								onChange={(e) => setBrowserViewportSize(e.target.value)}
-								style={{
-									width: "100%",
-									padding: "4px 8px",
-									backgroundColor: "var(--vscode-input-background)",
-									color: "var(--vscode-input-foreground)",
-									border: "1px solid var(--vscode-input-border)",
-									borderRadius: "2px",
-									height: "28px",
-								}}>
-								<option value="1280x800">Large Desktop (1280x800)</option>
-								<option value="900x600">Small Desktop (900x600)</option>
-								<option value="768x1024">Tablet (768x1024)</option>
-								<option value="360x640">Mobile (360x640)</option>
-							</select>
-							<p
+				<div style={{ marginBottom: 40 }}>
+					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Browser Settings</h3>
+					<div style={{ marginBottom: 15 }}>
+						<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>Viewport size</label>
+						<select
+							value={browserViewportSize}
+							onChange={(e) => setBrowserViewportSize(e.target.value)}
+							style={{
+								width: "100%",
+								padding: "4px 8px",
+								backgroundColor: "var(--vscode-input-background)",
+								color: "var(--vscode-input-foreground)",
+								border: "1px solid var(--vscode-input-border)",
+								borderRadius: "2px",
+								height: "28px",
+							}}>
+							<option value="1280x800">Large Desktop (1280x800)</option>
+							<option value="900x600">Small Desktop (900x600)</option>
+							<option value="768x1024">Tablet (768x1024)</option>
+							<option value="360x640">Mobile (360x640)</option>
+						</select>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							Select the viewport size for browser interactions. This affects how websites are displayed
+							and interacted with.
+						</p>
+					</div>
+
+					<div style={{ marginBottom: 15 }}>
+						<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
+							<span style={{ fontWeight: "500", minWidth: "100px" }}>Screenshot quality</span>
+							<input
+								type="range"
+								min="1"
+								max="100"
+								step="1"
+								value={screenshotQuality ?? 75}
+								onChange={(e) => setScreenshotQuality(parseInt(e.target.value))}
 								style={{
-									fontSize: "12px",
-									marginTop: "5px",
-									color: "var(--vscode-descriptionForeground)",
-								}}>
-								Select the viewport size for browser interactions. This affects how websites are
-								displayed and interacted with.
-							</p>
+									flexGrow: 1,
+									accentColor: "var(--vscode-button-background)",
+									height: "2px",
+								}}
+							/>
+							<span style={{ minWidth: "35px", textAlign: "left" }}>{screenshotQuality ?? 75}%</span>
 						</div>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							Adjust the WebP quality of browser screenshots. Higher values provide clearer screenshots
+							but increase token usage.
+						</p>
+					</div>
+				</div>
 
-						<div style={{ marginBottom: 15 }}>
+				<div style={{ marginBottom: 40 }}>
+					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Notification Settings</h3>
+					<div style={{ marginBottom: 15 }}>
+						<VSCodeCheckbox checked={soundEnabled} onChange={(e: any) => setSoundEnabled(e.target.checked)}>
+							<span style={{ fontWeight: "500" }}>Enable sound effects</span>
+						</VSCodeCheckbox>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							When enabled, Cline will play sound effects for notifications and events.
+						</p>
+					</div>
+					{soundEnabled && (
+						<div style={{ marginLeft: 0 }}>
 							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<span style={{ fontWeight: "500", minWidth: "100px" }}>Screenshot quality</span>
+								<span style={{ fontWeight: "500", minWidth: "100px" }}>Volume</span>
 								<input
 									type="range"
-									min="1"
-									max="100"
-									step="1"
-									value={screenshotQuality ?? 75}
-									onChange={(e) => setScreenshotQuality(parseInt(e.target.value))}
+									min="0"
+									max="1"
+									step="0.01"
+									value={soundVolume ?? 0.5}
+									onChange={(e) => setSoundVolume(parseFloat(e.target.value))}
 									style={{
 										flexGrow: 1,
 										accentColor: "var(--vscode-button-background)",
 										height: "2px",
 									}}
+									aria-label="Volume"
 								/>
-								<span style={{ minWidth: "35px", textAlign: "left" }}>{screenshotQuality ?? 75}%</span>
+								<span style={{ minWidth: "35px", textAlign: "left" }}>
+									{((soundVolume ?? 0.5) * 100).toFixed(0)}%
+								</span>
 							</div>
-							<p
-								style={{
-									fontSize: "12px",
-									marginTop: "5px",
-									color: "var(--vscode-descriptionForeground)",
-								}}>
-								Adjust the WebP quality of browser screenshots. Higher values provide clearer
-								screenshots but increase token usage.
-							</p>
 						</div>
-					</div>
+					)}
+				</div>
 
-					<div style={{ marginBottom: 5 }}>
-						<div style={{ marginBottom: 10 }}>
-							<h3 style={{ color: "var(--vscode-foreground)", margin: 0, marginBottom: 15 }}>
-								Notification Settings
-							</h3>
-							<VSCodeCheckbox
-								checked={soundEnabled}
-								onChange={(e: any) => setSoundEnabled(e.target.checked)}>
-								<span style={{ fontWeight: "500" }}>Enable sound effects</span>
-							</VSCodeCheckbox>
-							<p
+				<div style={{ marginBottom: 40 }}>
+					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Advanced Settings</h3>
+					<div style={{ marginBottom: 15 }}>
+						<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
+							<span style={{ fontWeight: "500", minWidth: "150px" }}>Terminal output limit</span>
+							<input
+								type="range"
+								min="100"
+								max="5000"
+								step="100"
+								value={terminalOutputLineLimit ?? 500}
+								onChange={(e) => setTerminalOutputLineLimit(parseInt(e.target.value))}
 								style={{
-									fontSize: "12px",
-									marginTop: "5px",
-									color: "var(--vscode-descriptionForeground)",
-								}}>
-								When enabled, Cline will play sound effects for notifications and events.
-							</p>
+									flexGrow: 1,
+									accentColor: "var(--vscode-button-background)",
+									height: "2px",
+								}}
+							/>
+							<span style={{ minWidth: "45px", textAlign: "left" }}>
+								{terminalOutputLineLimit ?? 500}
+							</span>
 						</div>
-						{soundEnabled && (
-							<div style={{ marginLeft: 0 }}>
+						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
+							Maximum number of lines to include in terminal output when executing commands. When exceeded
+							lines will be removed from the middle, saving tokens.
+						</p>
+					</div>
+
+					<div style={{ marginBottom: 15 }}>
+						<VSCodeCheckbox
+							checked={diffEnabled}
+							onChange={(e: any) => {
+								setDiffEnabled(e.target.checked)
+								if (!e.target.checked) {
+									// Reset experimental strategy when diffs are disabled
+									setExperimentalDiffStrategy(false)
+								}
+							}}>
+							<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
+						</VSCodeCheckbox>
+						<p
+							style={{
+								fontSize: "12px",
+								marginTop: "5px",
+								color: "var(--vscode-descriptionForeground)",
+							}}>
+							When enabled, Cline will be able to edit files more quickly and will automatically reject
+							truncated full-file writes. Works best with the latest Claude 3.5 Sonnet model.
+						</p>
+
+						{diffEnabled && (
+							<div style={{ marginTop: 10 }}>
+								<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
+									<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
+									<VSCodeCheckbox
+										checked={experimentalDiffStrategy}
+										onChange={(e: any) => setExperimentalDiffStrategy(e.target.checked)}>
+										<span style={{ fontWeight: "500" }}>
+											Use experimental unified diff strategy
+										</span>
+									</VSCodeCheckbox>
+								</div>
+								<p
+									style={{
+										fontSize: "12px",
+										marginBottom: 15,
+										color: "var(--vscode-descriptionForeground)",
+									}}>
+									Enable the experimental unified diff strategy. This strategy might reduce the number
+									of retries caused by model errors but may cause unexpected behavior or incorrect
+									edits. Only enable if you understand the risks and are willing to carefully review
+									all changes.
+								</p>
+
 								<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-									<span style={{ fontWeight: "500", minWidth: "100px" }}>Volume</span>
+									<span style={{ fontWeight: "500", minWidth: "100px" }}>Match precision</span>
 									<input
 										type="range"
-										min="0"
+										min="0.8"
 										max="1"
-										step="0.01"
-										value={soundVolume ?? 0.5}
-										onChange={(e) => setSoundVolume(parseFloat(e.target.value))}
+										step="0.005"
+										value={fuzzyMatchThreshold ?? 1.0}
+										onChange={(e) => {
+											setFuzzyMatchThreshold(parseFloat(e.target.value))
+										}}
 										style={{
 											flexGrow: 1,
 											accentColor: "var(--vscode-button-background)",
 											height: "2px",
 										}}
-										aria-label="Volume"
 									/>
 									<span style={{ minWidth: "35px", textAlign: "left" }}>
-										{((soundVolume ?? 0.5) * 100).toFixed(0)}%
+										{Math.round((fuzzyMatchThreshold || 1) * 100)}%
 									</span>
 								</div>
+								<p
+									style={{
+										fontSize: "12px",
+										marginTop: "5px",
+										color: "var(--vscode-descriptionForeground)",
+									}}>
+									This slider controls how precisely code sections must match when applying diffs.
+									Lower values allow more flexible matching but increase the risk of incorrect
+									replacements. Use values below 100% with extreme caution.
+								</p>
 							</div>
 						)}
 					</div>
 				</div>
 
-				{IS_DEV && (
-					<>
-						<div style={{ marginTop: "10px", marginBottom: "4px" }}>Debug</div>
-						<VSCodeButton onClick={handleResetState} style={{ marginTop: "5px", width: "auto" }}>
-							Reset State
-						</VSCodeButton>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							This will reset all global state and secret storage in the extension.
-						</p>
-					</>
-				)}
-
 				<div
 					style={{
 						textAlign: "center",
@@ -763,7 +643,25 @@ const SettingsView = ({ onDone }: SettingsViewProps) => {
 							reddit.com/r/roocline
 						</VSCodeLink>
 					</p>
-					<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0 }}>v{version}</p>
+					<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0, marginBottom: 100 }}>
+						v{version}
+					</p>
+
+					<p
+						style={{
+							fontSize: "12px",
+							marginTop: "5px",
+							color: "var(--vscode-descriptionForeground)",
+						}}>
+						This will reset all global state and secret storage in the extension.
+					</p>
+
+					<VSCodeButton
+						onClick={handleResetState}
+						appearance="secondary"
+						style={{ marginTop: "5px", width: "auto" }}>
+						Reset State
+					</VSCodeButton>
 				</div>
 			</div>
 		</div>