Просмотр исходного кода

Merge remote-tracking branch 'origin/main' into human-relay

Matt Rubens 10 месяцев назад
Родитель
Сommit
73ac64924e
49 измененных файлов с 2419 добавлено и 1816 удалено
  1. 5 0
      .changeset/wise-pears-join.md
  2. 0 47
      package.json
  3. 0 1
      src/activate/index.ts
  4. 0 81
      src/activate/registerTerminalActions.ts
  5. 14 0
      src/api/providers/__tests__/openai.test.ts
  6. 24 5
      src/api/providers/lmstudio.ts
  7. 8 3
      src/api/providers/openai.ts
  8. 1 5
      src/api/providers/openrouter.ts
  9. 0 4
      src/api/providers/requesty.ts
  10. 4 2
      src/core/Cline.ts
  11. 243 0
      src/core/__tests__/contextProxy.test.ts
  12. 154 0
      src/core/contextProxy.ts
  13. 129 427
      src/core/webview/ClineProvider.ts
  14. 158 0
      src/core/webview/__tests__/ClineProvider.test.ts
  15. 2 3
      src/extension.ts
  16. 2 0
      src/shared/ExtensionMessage.ts
  17. 1 0
      src/shared/WebviewMessage.ts
  18. 48 1
      src/shared/api.ts
  19. 16 19
      src/shared/checkExistApiConfig.ts
  20. 5 0
      src/shared/checkpoints.ts
  21. 99 86
      src/shared/globalState.ts
  22. 0 1
      webview-ui/src/components/common/MermaidBlock.tsx
  23. 51 88
      webview-ui/src/components/history/HistoryPreview.tsx
  24. 17 98
      webview-ui/src/components/history/HistoryView.tsx
  25. 78 0
      webview-ui/src/components/history/useTaskSearch.ts
  26. 4 23
      webview-ui/src/components/mcp/McpView.tsx
  27. 2 4
      webview-ui/src/components/prompts/PromptsView.tsx
  28. 176 0
      webview-ui/src/components/settings/AdvancedSettings.tsx
  29. 170 181
      webview-ui/src/components/settings/ApiConfigManager.tsx
  30. 73 2
      webview-ui/src/components/settings/ApiOptions.tsx
  31. 252 0
      webview-ui/src/components/settings/AutoApproveSettings.tsx
  32. 105 0
      webview-ui/src/components/settings/BrowserSettings.tsx
  33. 82 0
      webview-ui/src/components/settings/CheckpointSettings.tsx
  34. 10 21
      webview-ui/src/components/settings/ExperimentalFeature.tsx
  35. 53 0
      webview-ui/src/components/settings/ExperimentalSettings.tsx
  36. 69 0
      webview-ui/src/components/settings/NotificationSettings.tsx
  37. 9 0
      webview-ui/src/components/settings/Section.tsx
  38. 15 0
      webview-ui/src/components/settings/SectionHeader.tsx
  39. 36 0
      webview-ui/src/components/settings/SettingsFooter.tsx
  40. 224 693
      webview-ui/src/components/settings/SettingsView.tsx
  41. 3 3
      webview-ui/src/components/settings/TemperatureControl.tsx
  42. 18 0
      webview-ui/src/components/settings/__tests__/SettingsView.test.tsx
  43. 7 2
      webview-ui/src/components/settings/styles.ts
  44. 10 0
      webview-ui/src/components/settings/types.ts
  45. 15 14
      webview-ui/src/components/ui/__tests__/select-dropdown.test.tsx
  46. 21 2
      webview-ui/src/components/ui/select-dropdown.tsx
  47. 1 0
      webview-ui/src/context/ExtensionStateContext.tsx
  48. 1 0
      webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx
  49. 4 0
      webview-ui/src/index.css

+ 5 - 0
.changeset/wise-pears-join.md

@@ -0,0 +1,5 @@
+---
+"roo-cline": patch
+---
+
+Improved observability of openai compatible APIs, by sending x-title and http-referer headers, as per Open Router standard.

+ 0 - 47
package.json

@@ -128,31 +128,6 @@
 				"command": "roo-cline.addToContext",
 				"title": "Roo Code: Add To Context",
 				"category": "Roo Code"
-			},
-			{
-				"command": "roo-cline.terminalAddToContext",
-				"title": "Roo Code: Add Terminal Content to Context",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalFixCommand",
-				"title": "Roo Code: Fix This Command",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalExplainCommand",
-				"title": "Roo Code: Explain This Command",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalFixCommandInCurrentTask",
-				"title": "Roo Code: Fix This Command (Current Task)",
-				"category": "Terminal"
-			},
-			{
-				"command": "roo-cline.terminalExplainCommandInCurrentTask",
-				"title": "Roo Code: Explain This Command (Current Task)",
-				"category": "Terminal"
 			}
 		],
 		"menus": {
@@ -178,28 +153,6 @@
 					"group": "Roo Code@4"
 				}
 			],
-			"terminal/context": [
-				{
-					"command": "roo-cline.terminalAddToContext",
-					"group": "Roo Code@1"
-				},
-				{
-					"command": "roo-cline.terminalFixCommand",
-					"group": "Roo Code@2"
-				},
-				{
-					"command": "roo-cline.terminalExplainCommand",
-					"group": "Roo Code@3"
-				},
-				{
-					"command": "roo-cline.terminalFixCommandInCurrentTask",
-					"group": "Roo Code@5"
-				},
-				{
-					"command": "roo-cline.terminalExplainCommandInCurrentTask",
-					"group": "Roo Code@6"
-				}
-			],
 			"view/title": [
 				{
 					"command": "roo-cline.plusButtonClicked",

+ 0 - 1
src/activate/index.ts

@@ -1,4 +1,3 @@
 export { handleUri } from "./handleUri"
 export { registerCommands } from "./registerCommands"
 export { registerCodeActions } from "./registerCodeActions"
-export { registerTerminalActions } from "./registerTerminalActions"

+ 0 - 81
src/activate/registerTerminalActions.ts

@@ -1,81 +0,0 @@
-import * as vscode from "vscode"
-import { ClineProvider } from "../core/webview/ClineProvider"
-import { TerminalManager } from "../integrations/terminal/TerminalManager"
-
-const TERMINAL_COMMAND_IDS = {
-	ADD_TO_CONTEXT: "roo-cline.terminalAddToContext",
-	FIX: "roo-cline.terminalFixCommand",
-	FIX_IN_CURRENT_TASK: "roo-cline.terminalFixCommandInCurrentTask",
-	EXPLAIN: "roo-cline.terminalExplainCommand",
-	EXPLAIN_IN_CURRENT_TASK: "roo-cline.terminalExplainCommandInCurrentTask",
-} as const
-
-export const registerTerminalActions = (context: vscode.ExtensionContext) => {
-	const terminalManager = new TerminalManager()
-
-	registerTerminalAction(context, terminalManager, TERMINAL_COMMAND_IDS.ADD_TO_CONTEXT, "TERMINAL_ADD_TO_CONTEXT")
-
-	registerTerminalActionPair(
-		context,
-		terminalManager,
-		TERMINAL_COMMAND_IDS.FIX,
-		"TERMINAL_FIX",
-		"What would you like Roo to fix?",
-	)
-
-	registerTerminalActionPair(
-		context,
-		terminalManager,
-		TERMINAL_COMMAND_IDS.EXPLAIN,
-		"TERMINAL_EXPLAIN",
-		"What would you like Roo to explain?",
-	)
-}
-
-const registerTerminalAction = (
-	context: vscode.ExtensionContext,
-	terminalManager: TerminalManager,
-	command: string,
-	promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
-	inputPrompt?: string,
-) => {
-	context.subscriptions.push(
-		vscode.commands.registerCommand(command, async (args: any) => {
-			let content = args.selection
-			if (!content || content === "") {
-				content = await terminalManager.getTerminalContents(promptType === "TERMINAL_ADD_TO_CONTEXT" ? -1 : 1)
-			}
-
-			if (!content) {
-				vscode.window.showWarningMessage("No terminal content selected")
-				return
-			}
-
-			const params: Record<string, any> = {
-				terminalContent: content,
-			}
-
-			if (inputPrompt) {
-				params.userInput =
-					(await vscode.window.showInputBox({
-						prompt: inputPrompt,
-					})) ?? ""
-			}
-
-			await ClineProvider.handleTerminalAction(command, promptType, params)
-		}),
-	)
-}
-
-const registerTerminalActionPair = (
-	context: vscode.ExtensionContext,
-	terminalManager: TerminalManager,
-	baseCommand: string,
-	promptType: "TERMINAL_ADD_TO_CONTEXT" | "TERMINAL_FIX" | "TERMINAL_EXPLAIN",
-	inputPrompt?: string,
-) => {
-	// Register new task version
-	registerTerminalAction(context, terminalManager, baseCommand, promptType, inputPrompt)
-	// Register current task version
-	registerTerminalAction(context, terminalManager, `${baseCommand}InCurrentTask`, promptType, inputPrompt)
-}

+ 14 - 0
src/api/providers/__tests__/openai.test.ts

@@ -90,6 +90,20 @@ describe("OpenAiHandler", () => {
 			})
 			expect(handlerWithCustomUrl).toBeInstanceOf(OpenAiHandler)
 		})
+
+		it("should set default headers correctly", () => {
+			// Get the mock constructor from the jest mock system
+			const openAiMock = jest.requireMock("openai").default
+
+			expect(openAiMock).toHaveBeenCalledWith({
+				baseURL: expect.any(String),
+				apiKey: expect.any(String),
+				defaultHeaders: {
+					"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
+					"X-Title": "Roo Code",
+				},
+			})
+		})
 	})
 
 	describe("createMessage", () => {

+ 24 - 5
src/api/providers/lmstudio.ts

@@ -30,13 +30,24 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
 		]
 
 		try {
-			const stream = await this.client.chat.completions.create({
+			// Create params object with optional draft model
+			const params: any = {
 				model: this.getModel().id,
 				messages: openAiMessages,
 				temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE,
 				stream: true,
-			})
-			for await (const chunk of stream) {
+			}
+
+			// Add draft model if speculative decoding is enabled and a draft model is specified
+			if (this.options.lmStudioSpeculativeDecodingEnabled && this.options.lmStudioDraftModelId) {
+				params.draft_model = this.options.lmStudioDraftModelId
+			}
+
+			const results = await this.client.chat.completions.create(params)
+
+			// Stream handling
+			// @ts-ignore
+			for await (const chunk of results) {
 				const delta = chunk.choices[0]?.delta
 				if (delta?.content) {
 					yield {
@@ -62,12 +73,20 @@ export class LmStudioHandler extends BaseProvider implements SingleCompletionHan
 
 	async completePrompt(prompt: string): Promise<string> {
 		try {
-			const response = await this.client.chat.completions.create({
+			// Create params object with optional draft model
+			const params: any = {
 				model: this.getModel().id,
 				messages: [{ role: "user", content: prompt }],
 				temperature: this.options.modelTemperature ?? LMSTUDIO_DEFAULT_TEMPERATURE,
 				stream: false,
-			})
+			}
+
+			// Add draft model if speculative decoding is enabled and a draft model is specified
+			if (this.options.lmStudioSpeculativeDecodingEnabled && this.options.lmStudioDraftModelId) {
+				params.draft_model = this.options.lmStudioDraftModelId
+			}
+
+			const response = await this.client.chat.completions.create(params)
 			return response.choices[0]?.message.content || ""
 		} catch (error) {
 			throw new Error(

+ 8 - 3
src/api/providers/openai.ts

@@ -16,10 +16,14 @@ import { ApiStream, ApiStreamUsageChunk } from "../transform/stream"
 import { BaseProvider } from "./base-provider"
 
 const DEEP_SEEK_DEFAULT_TEMPERATURE = 0.6
-export interface OpenAiHandlerOptions extends ApiHandlerOptions {
-	defaultHeaders?: Record<string, string>
+
+export const defaultHeaders = {
+	"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
+	"X-Title": "Roo Code",
 }
 
+export interface OpenAiHandlerOptions extends ApiHandlerOptions {}
+
 export class OpenAiHandler extends BaseProvider implements SingleCompletionHandler {
 	protected options: OpenAiHandlerOptions
 	private client: OpenAI
@@ -47,9 +51,10 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
 				baseURL,
 				apiKey,
 				apiVersion: this.options.azureApiVersion || azureOpenAiDefaultApiVersion,
+				defaultHeaders,
 			})
 		} else {
-			this.client = new OpenAI({ baseURL, apiKey, defaultHeaders: this.options.defaultHeaders })
+			this.client = new OpenAI({ baseURL, apiKey, defaultHeaders })
 		}
 	}
 

+ 1 - 5
src/api/providers/openrouter.ts

@@ -13,6 +13,7 @@ import { convertToR1Format } from "../transform/r1-format"
 import { DEEP_SEEK_DEFAULT_TEMPERATURE } from "./constants"
 import { getModelParams, SingleCompletionHandler } from ".."
 import { BaseProvider } from "./base-provider"
+import { defaultHeaders } from "./openai"
 
 // Add custom interface for OpenRouter params.
 type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
@@ -37,11 +38,6 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
 		const baseURL = this.options.openRouterBaseUrl || "https://openrouter.ai/api/v1"
 		const apiKey = this.options.openRouterApiKey ?? "not-provided"
 
-		const defaultHeaders = {
-			"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
-			"X-Title": "Roo Code",
-		}
-
 		this.client = new OpenAI({ baseURL, apiKey, defaultHeaders })
 	}
 

+ 0 - 4
src/api/providers/requesty.ts

@@ -16,10 +16,6 @@ export class RequestyHandler extends OpenAiHandler {
 			openAiModelId: options.requestyModelId ?? requestyDefaultModelId,
 			openAiBaseUrl: "https://router.requesty.ai/v1",
 			openAiCustomModelInfo: options.requestyModelInfo ?? requestyModelInfoSaneDefaults,
-			defaultHeaders: {
-				"HTTP-Referer": "https://github.com/RooVetGit/Roo-Cline",
-				"X-Title": "Roo Code",
-			},
 		})
 	}
 

+ 4 - 2
src/core/Cline.ts

@@ -10,6 +10,7 @@ import getFolderSize from "get-folder-size"
 import * as path from "path"
 import { serializeError } from "serialize-error"
 import * as vscode from "vscode"
+
 import { ApiHandler, buildApiHandler } from "../api"
 import { ApiStream } from "../api/transform/stream"
 import { DIFF_VIEW_URI_SCHEME, DiffViewProvider } from "../integrations/editor/DiffViewProvider"
@@ -31,6 +32,7 @@ import { UrlContentFetcher } from "../services/browser/UrlContentFetcher"
 import { listFiles } from "../services/glob/list-files"
 import { regexSearchFiles } from "../services/ripgrep"
 import { parseSourceCodeForDefinitionsTopLevel } from "../services/tree-sitter"
+import { CheckpointStorage } from "../shared/checkpoints"
 import { ApiConfiguration } from "../shared/api"
 import { findLastIndex } from "../shared/array"
 import { combineApiRequests } from "../shared/combineApiRequests"
@@ -81,7 +83,7 @@ export type ClineOptions = {
 	customInstructions?: string
 	enableDiff?: boolean
 	enableCheckpoints?: boolean
-	checkpointStorage?: "task" | "workspace"
+	checkpointStorage?: CheckpointStorage
 	fuzzyMatchThreshold?: number
 	task?: string
 	images?: string[]
@@ -121,7 +123,7 @@ export class Cline {
 
 	// checkpoints
 	private enableCheckpoints: boolean
-	private checkpointStorage: "task" | "workspace"
+	private checkpointStorage: CheckpointStorage
 	private checkpointService?: RepoPerTaskCheckpointService | RepoPerWorkspaceCheckpointService
 
 	// streaming

+ 243 - 0
src/core/__tests__/contextProxy.test.ts

@@ -0,0 +1,243 @@
+import * as vscode from "vscode"
+import { ContextProxy } from "../contextProxy"
+import { logger } from "../../utils/logging"
+import { GLOBAL_STATE_KEYS, SECRET_KEYS } from "../../shared/globalState"
+import { ApiConfiguration } from "../../shared/api"
+
+// Mock shared/globalState
+jest.mock("../../shared/globalState", () => ({
+	GLOBAL_STATE_KEYS: ["apiProvider", "apiModelId", "mode"],
+	SECRET_KEYS: ["apiKey", "openAiApiKey"],
+	GlobalStateKey: {},
+	SecretKey: {},
+}))
+
+// Mock shared/api
+jest.mock("../../shared/api", () => ({
+	API_CONFIG_KEYS: ["apiProvider", "apiModelId"],
+	ApiConfiguration: {},
+}))
+
+// Mock VSCode API
+jest.mock("vscode", () => ({
+	Uri: {
+		file: jest.fn((path) => ({ path })),
+	},
+	ExtensionMode: {
+		Development: 1,
+		Production: 2,
+		Test: 3,
+	},
+}))
+
+describe("ContextProxy", () => {
+	let proxy: ContextProxy
+	let mockContext: any
+	let mockGlobalState: any
+	let mockSecrets: any
+
+	beforeEach(() => {
+		// Reset mocks
+		jest.clearAllMocks()
+
+		// Mock globalState
+		mockGlobalState = {
+			get: jest.fn(),
+			update: jest.fn().mockResolvedValue(undefined),
+		}
+
+		// Mock secrets
+		mockSecrets = {
+			get: jest.fn().mockResolvedValue("test-secret"),
+			store: jest.fn().mockResolvedValue(undefined),
+			delete: jest.fn().mockResolvedValue(undefined),
+		}
+
+		// Mock the extension context
+		mockContext = {
+			globalState: mockGlobalState,
+			secrets: mockSecrets,
+			extensionUri: { path: "/test/extension" },
+			extensionPath: "/test/extension",
+			globalStorageUri: { path: "/test/storage" },
+			logUri: { path: "/test/logs" },
+			extension: { packageJSON: { version: "1.0.0" } },
+			extensionMode: vscode.ExtensionMode.Development,
+		}
+
+		// Create proxy instance
+		proxy = new ContextProxy(mockContext)
+	})
+
+	describe("read-only pass-through properties", () => {
+		it("should return extension properties from the original context", () => {
+			expect(proxy.extensionUri).toBe(mockContext.extensionUri)
+			expect(proxy.extensionPath).toBe(mockContext.extensionPath)
+			expect(proxy.globalStorageUri).toBe(mockContext.globalStorageUri)
+			expect(proxy.logUri).toBe(mockContext.logUri)
+			expect(proxy.extension).toBe(mockContext.extension)
+			expect(proxy.extensionMode).toBe(mockContext.extensionMode)
+		})
+	})
+
+	describe("constructor", () => {
+		it("should initialize state cache with all global state keys", () => {
+			expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length)
+			for (const key of GLOBAL_STATE_KEYS) {
+				expect(mockGlobalState.get).toHaveBeenCalledWith(key)
+			}
+		})
+
+		it("should initialize secret cache with all secret keys", () => {
+			expect(mockSecrets.get).toHaveBeenCalledTimes(SECRET_KEYS.length)
+			for (const key of SECRET_KEYS) {
+				expect(mockSecrets.get).toHaveBeenCalledWith(key)
+			}
+		})
+	})
+
+	describe("getGlobalState", () => {
+		it("should return value from cache when it exists", async () => {
+			// Manually set a value in the cache
+			await proxy.updateGlobalState("test-key", "cached-value")
+
+			// Should return the cached value
+			const result = proxy.getGlobalState("test-key")
+			expect(result).toBe("cached-value")
+
+			// Original context should be called once during updateGlobalState
+			expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length) // Only from initialization
+		})
+
+		it("should handle default values correctly", async () => {
+			// No value in cache
+			const result = proxy.getGlobalState("unknown-key", "default-value")
+			expect(result).toBe("default-value")
+		})
+	})
+
+	describe("updateGlobalState", () => {
+		it("should update state directly in original context", async () => {
+			await proxy.updateGlobalState("test-key", "new-value")
+
+			// Should have called original context
+			expect(mockGlobalState.update).toHaveBeenCalledWith("test-key", "new-value")
+
+			// Should have stored the value in cache
+			const storedValue = await proxy.getGlobalState("test-key")
+			expect(storedValue).toBe("new-value")
+		})
+	})
+
+	describe("getSecret", () => {
+		it("should return value from cache when it exists", async () => {
+			// Manually set a value in the cache
+			await proxy.storeSecret("api-key", "cached-secret")
+
+			// Should return the cached value
+			const result = proxy.getSecret("api-key")
+			expect(result).toBe("cached-secret")
+		})
+	})
+
+	describe("storeSecret", () => {
+		it("should store secret directly in original context", async () => {
+			await proxy.storeSecret("api-key", "new-secret")
+
+			// Should have called original context
+			expect(mockSecrets.store).toHaveBeenCalledWith("api-key", "new-secret")
+
+			// Should have stored the value in cache
+			const storedValue = await proxy.getSecret("api-key")
+			expect(storedValue).toBe("new-secret")
+		})
+
+		it("should handle undefined value for secret deletion", async () => {
+			await proxy.storeSecret("api-key", undefined)
+
+			// Should have called delete on original context
+			expect(mockSecrets.delete).toHaveBeenCalledWith("api-key")
+
+			// Should have stored undefined in cache
+			const storedValue = await proxy.getSecret("api-key")
+			expect(storedValue).toBeUndefined()
+		})
+
+		describe("getApiConfiguration", () => {
+			it("should combine global state and secrets into a single ApiConfiguration object", async () => {
+				// Mock data in state cache
+				await proxy.updateGlobalState("apiProvider", "anthropic")
+				await proxy.updateGlobalState("apiModelId", "test-model")
+				// Mock data in secrets cache
+				await proxy.storeSecret("apiKey", "test-api-key")
+
+				const config = proxy.getApiConfiguration()
+
+				// Should contain values from global state
+				expect(config.apiProvider).toBe("anthropic")
+				expect(config.apiModelId).toBe("test-model")
+				// Should contain values from secrets
+				expect(config.apiKey).toBe("test-api-key")
+			})
+
+			it("should handle special case for apiProvider defaulting", async () => {
+				// Clear apiProvider but set apiKey
+				await proxy.updateGlobalState("apiProvider", undefined)
+				await proxy.storeSecret("apiKey", "test-api-key")
+
+				const config = proxy.getApiConfiguration()
+
+				// Should default to anthropic when apiKey exists
+				expect(config.apiProvider).toBe("anthropic")
+
+				// Clear both apiProvider and apiKey
+				await proxy.updateGlobalState("apiProvider", undefined)
+				await proxy.storeSecret("apiKey", undefined)
+
+				const configWithoutKey = proxy.getApiConfiguration()
+
+				// Should default to openrouter when no apiKey exists
+				expect(configWithoutKey.apiProvider).toBe("openrouter")
+			})
+		})
+
+		describe("updateApiConfiguration", () => {
+			it("should update both global state and secrets", async () => {
+				const apiConfig: ApiConfiguration = {
+					apiProvider: "anthropic",
+					apiModelId: "claude-latest",
+					apiKey: "test-api-key",
+				}
+
+				await proxy.updateApiConfiguration(apiConfig)
+
+				// Should update global state
+				expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "anthropic")
+				expect(mockGlobalState.update).toHaveBeenCalledWith("apiModelId", "claude-latest")
+				// Should update secrets
+				expect(mockSecrets.store).toHaveBeenCalledWith("apiKey", "test-api-key")
+
+				// Check that values are in cache
+				expect(proxy.getGlobalState("apiProvider")).toBe("anthropic")
+				expect(proxy.getGlobalState("apiModelId")).toBe("claude-latest")
+				expect(proxy.getSecret("apiKey")).toBe("test-api-key")
+			})
+
+			it("should ignore keys that aren't in either GLOBAL_STATE_KEYS or SECRET_KEYS", async () => {
+				// Use type assertion to add an invalid key
+				const apiConfig = {
+					apiProvider: "anthropic",
+					invalidKey: "should be ignored",
+				} as ApiConfiguration & { invalidKey: string }
+
+				await proxy.updateApiConfiguration(apiConfig)
+
+				// Should update keys in GLOBAL_STATE_KEYS
+				expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "anthropic")
+				// Should not call update/store for invalid keys
+				expect(mockGlobalState.update).not.toHaveBeenCalledWith("invalidKey", expect.anything())
+				expect(mockSecrets.store).not.toHaveBeenCalledWith("invalidKey", expect.anything())
+			})
+		})
+	})
+})

+ 154 - 0
src/core/contextProxy.ts

@@ -0,0 +1,154 @@
+import * as vscode from "vscode"
+import { logger } from "../utils/logging"
+import { ApiConfiguration, API_CONFIG_KEYS } from "../shared/api"
+import { GLOBAL_STATE_KEYS, SECRET_KEYS, GlobalStateKey, SecretKey } from "../shared/globalState"
+
+export class ContextProxy {
+	private readonly originalContext: vscode.ExtensionContext
+	private stateCache: Map<string, any>
+	private secretCache: Map<string, string | undefined>
+
+	constructor(context: vscode.ExtensionContext) {
+		// Initialize properties first
+		this.originalContext = context
+		this.stateCache = new Map()
+		this.secretCache = new Map()
+
+		// Initialize state cache with all defined global state keys
+		this.initializeStateCache()
+
+		// Initialize secret cache with all defined secret keys
+		this.initializeSecretCache()
+
+		logger.debug("ContextProxy created")
+	}
+
+	// Helper method to initialize state cache
+	private initializeStateCache(): void {
+		for (const key of GLOBAL_STATE_KEYS) {
+			try {
+				const value = this.originalContext.globalState.get(key)
+				this.stateCache.set(key, value)
+			} catch (error) {
+				logger.error(`Error loading global ${key}: ${error instanceof Error ? error.message : String(error)}`)
+			}
+		}
+	}
+
+	// Helper method to initialize secret cache
+	private initializeSecretCache(): void {
+		for (const key of SECRET_KEYS) {
+			// Get actual value and update cache when promise resolves
+			;(this.originalContext.secrets.get(key) as Promise<string | undefined>)
+				.then((value) => {
+					this.secretCache.set(key, value)
+				})
+				.catch((error: Error) => {
+					logger.error(`Error loading secret ${key}: ${error.message}`)
+				})
+		}
+	}
+
+	get extensionUri(): vscode.Uri {
+		return this.originalContext.extensionUri
+	}
+	get extensionPath(): string {
+		return this.originalContext.extensionPath
+	}
+	get globalStorageUri(): vscode.Uri {
+		return this.originalContext.globalStorageUri
+	}
+	get logUri(): vscode.Uri {
+		return this.originalContext.logUri
+	}
+	get extension(): vscode.Extension<any> | undefined {
+		return this.originalContext.extension
+	}
+	get extensionMode(): vscode.ExtensionMode {
+		return this.originalContext.extensionMode
+	}
+
+	getGlobalState<T>(key: string): T | undefined
+	getGlobalState<T>(key: string, defaultValue: T): T
+	getGlobalState<T>(key: string, defaultValue?: T): T | undefined {
+		const value = this.stateCache.get(key) as T | undefined
+		return value !== undefined ? value : (defaultValue as T | undefined)
+	}
+
+	updateGlobalState<T>(key: string, value: T): Thenable<void> {
+		this.stateCache.set(key, value)
+		return this.originalContext.globalState.update(key, value)
+	}
+
+	getSecret(key: string): string | undefined {
+		return this.secretCache.get(key)
+	}
+	storeSecret(key: string, value?: string): Thenable<void> {
+		// Update cache
+		this.secretCache.set(key, value)
+		// Write directly to context
+		if (value === undefined) {
+			return this.originalContext.secrets.delete(key)
+		} else {
+			return this.originalContext.secrets.store(key, value)
+		}
+	}
+
+	/**
+	 * Gets a complete ApiConfiguration object by fetching values
+	 * from both global state and secrets storage
+	 */
+	getApiConfiguration(): ApiConfiguration {
+		// Create an empty ApiConfiguration object
+		const config: ApiConfiguration = {}
+
+		// Add all API-related keys from global state
+		for (const key of API_CONFIG_KEYS) {
+			const value = this.getGlobalState(key)
+			if (value !== undefined) {
+				// Use type assertion to avoid TypeScript error
+				;(config as any)[key] = value
+			}
+		}
+
+		// Add all secret values
+		for (const key of SECRET_KEYS) {
+			const value = this.getSecret(key)
+			if (value !== undefined) {
+				// Use type assertion to avoid TypeScript error
+				;(config as any)[key] = value
+			}
+		}
+
+		// Handle special case for apiProvider if needed (same logic as current implementation)
+		if (!config.apiProvider) {
+			if (config.apiKey) {
+				config.apiProvider = "anthropic"
+			} else {
+				config.apiProvider = "openrouter"
+			}
+		}
+
+		return config
+	}
+
+	/**
+	 * Updates an ApiConfiguration by persisting each property
+	 * to the appropriate storage (global state or secrets)
+	 */
+	async updateApiConfiguration(apiConfiguration: ApiConfiguration): Promise<void> {
+		const promises: Array<Thenable<void>> = []
+
+		// For each property, update the appropriate storage
+		Object.entries(apiConfiguration).forEach(([key, value]) => {
+			if (SECRET_KEYS.includes(key as SecretKey)) {
+				promises.push(this.storeSecret(key, value))
+			} else if (API_CONFIG_KEYS.includes(key as GlobalStateKey)) {
+				promises.push(this.updateGlobalState(key, value))
+			}
+			// Ignore keys that aren't in either list
+		})
+
+		await Promise.all(promises)
+	}
+}

+ 129 - 427
src/core/webview/ClineProvider.ts

@@ -9,15 +9,16 @@ import * as vscode from "vscode"
 import simpleGit from "simple-git"
 import { setPanel } from "../../activate/registerCommands"
 
-import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
+import { ApiConfiguration, ApiProvider, ModelInfo, API_CONFIG_KEYS } from "../../shared/api"
+import { CheckpointStorage } from "../../shared/checkpoints"
 import { findLast } from "../../shared/array"
 import { CustomSupportPrompts, supportPrompt } from "../../shared/support-prompt"
 import { GlobalFileNames } from "../../shared/globalFileNames"
-import type { SecretKey, GlobalStateKey } from "../../shared/globalState"
+import { SecretKey, GlobalStateKey, SECRET_KEYS, GLOBAL_STATE_KEYS } from "../../shared/globalState"
 import { HistoryItem } from "../../shared/HistoryItem"
 import { ApiConfigMeta, ExtensionMessage } from "../../shared/ExtensionMessage"
 import { checkoutDiffPayloadSchema, checkoutRestorePayloadSchema, WebviewMessage } from "../../shared/WebviewMessage"
-import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug } from "../../shared/modes"
+import { Mode, CustomModePrompts, PromptComponent, defaultModeSlug, ModeConfig } from "../../shared/modes"
 import { checkExistKey } from "../../shared/checkExistApiConfig"
 import { EXPERIMENT_IDS, experiments as Experiments, experimentDefault, ExperimentId } from "../../shared/experiments"
 import { downloadTask } from "../../integrations/misc/export-markdown"
@@ -35,6 +36,7 @@ import { getDiffStrategy } from "../diff/DiffStrategy"
 import { SYSTEM_PROMPT } from "../prompts/system"
 import { ConfigManager } from "../config/ConfigManager"
 import { CustomModesManager } from "../config/CustomModesManager"
+import { ContextProxy } from "../contextProxy"
 import { buildApiHandler } from "../../api"
 import { getOpenRouterModels } from "../../api/providers/openrouter"
 import { getGlamaModels } from "../../api/providers/glama"
@@ -66,6 +68,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	private workspaceTracker?: WorkspaceTracker
 	protected mcpHub?: McpHub // Change from private to protected
 	private latestAnnouncementId = "feb-27-2025-automatic-checkpoints" // update to some unique identifier when we add a new announcement
+	private contextProxy: ContextProxy
 	configManager: ConfigManager
 	customModesManager: CustomModesManager
 
@@ -74,6 +77,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		private readonly outputChannel: vscode.OutputChannel,
 	) {
 		this.outputChannel.appendLine("ClineProvider instantiated")
+		this.contextProxy = new ContextProxy(context)
 		ClineProvider.activeInstances.add(this)
 		this.workspaceTracker = new WorkspaceTracker(this)
 		this.configManager = new ConfigManager(this.context)
@@ -251,11 +255,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		webviewView.webview.options = {
 			// Allow scripts in the webview
 			enableScripts: true,
-			localResourceRoots: [this.context.extensionUri],
+			localResourceRoots: [this.contextProxy.extensionUri],
 		}
 
 		webviewView.webview.html =
-			this.context.extensionMode === vscode.ExtensionMode.Development
+			this.contextProxy.extensionMode === vscode.ExtensionMode.Development
 				? await this.getHMRHtmlContent(webviewView.webview)
 				: this.getHtmlContent(webviewView.webview)
 
@@ -323,11 +327,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 
 	public async initClineWithTask(task?: string, images?: string[]) {
 		await this.clearTask()
+
 		const {
 			apiConfiguration,
 			customModePrompts,
-			diffEnabled,
+			diffEnabled: enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			mode,
 			customInstructions: globalInstructions,
@@ -341,8 +347,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			provider: this,
 			apiConfiguration,
 			customInstructions: effectiveInstructions,
-			enableDiff: diffEnabled,
+			enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			task,
 			images,
@@ -356,8 +363,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const {
 			apiConfiguration,
 			customModePrompts,
-			diffEnabled,
+			diffEnabled: enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			mode,
 			customInstructions: globalInstructions,
@@ -367,12 +375,17 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const modePrompt = customModePrompts?.[mode] as PromptComponent
 		const effectiveInstructions = [globalInstructions, modePrompt?.customInstructions].filter(Boolean).join("\n\n")
 
+		// TODO: The `checkpointStorage` value should be derived from the
+		// task data on disk; the current setting could be different than
+		// the setting at the time the task was created.
+
 		this.cline = new Cline({
 			provider: this,
 			apiConfiguration,
 			customInstructions: effectiveInstructions,
-			enableDiff: diffEnabled,
+			enableDiff,
 			enableCheckpoints,
+			checkpointStorage,
 			fuzzyMatchThreshold,
 			historyItem,
 			experiments,
@@ -399,8 +412,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		}
 
 		const nonce = getNonce()
-		const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
-		const codiconsUri = getUri(webview, this.context.extensionUri, [
+		const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
+			"webview-ui",
+			"build",
+			"assets",
+			"index.css",
+		])
+		const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
 			"node_modules",
 			"@vscode",
 			"codicons",
@@ -466,15 +484,20 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		// then convert it to a uri we can use in the webview.
 
 		// The CSS file from the React build output
-		const stylesUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.css"])
+		const stylesUri = getUri(webview, this.contextProxy.extensionUri, [
+			"webview-ui",
+			"build",
+			"assets",
+			"index.css",
+		])
 		// The JS file from the React build output
-		const scriptUri = getUri(webview, this.context.extensionUri, ["webview-ui", "build", "assets", "index.js"])
+		const scriptUri = getUri(webview, this.contextProxy.extensionUri, ["webview-ui", "build", "assets", "index.js"])
 
 		// The codicon font from the React build output
 		// https://github.com/microsoft/vscode-extension-samples/blob/main/webview-codicons-sample/src/extension.ts
 		// we installed this package in the extension so that we can access it how its intended from the extension (the font file is likely bundled in vscode), and we just import the css fileinto our react app we don't have access to it
 		// don't forget to add font-src ${webview.cspSource};
-		const codiconsUri = getUri(webview, this.context.extensionUri, [
+		const codiconsUri = getUri(webview, this.contextProxy.extensionUri, [
 			"node_modules",
 			"@vscode",
 			"codicons",
@@ -1032,6 +1055,12 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 						await this.updateGlobalState("enableCheckpoints", enableCheckpoints)
 						await this.postStateToWebview()
 						break
+					case "checkpointStorage":
+						console.log(`[ClineProvider] checkpointStorage: ${message.text}`)
+						const checkpointStorage = message.text ?? "task"
+						await this.updateGlobalState("checkpointStorage", checkpointStorage)
+						await this.postStateToWebview()
+						break
 					case "browserViewportSize":
 						const browserViewportSize = message.text ?? "900x600"
 						await this.updateGlobalState("browserViewportSize", browserViewportSize)
@@ -1259,7 +1288,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 								// Try to get enhancement config first, fall back to current config
 								let configToUse: ApiConfiguration = apiConfiguration
 								if (enhancementApiConfigId) {
-									const config = listApiConfigMeta?.find((c) => c.id === enhancementApiConfigId)
+									const config = listApiConfigMeta?.find(
+										(c: ApiConfigMeta) => c.id === enhancementApiConfigId,
+									)
 									if (config?.name) {
 										const loadedConfig = await this.configManager.loadConfig(config.name)
 										if (loadedConfig.apiProvider) {
@@ -1657,104 +1688,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			}
 		}
 
-		const {
-			apiProvider,
-			apiModelId,
-			apiKey,
-			glamaModelId,
-			glamaModelInfo,
-			glamaApiKey,
-			openRouterApiKey,
-			awsAccessKey,
-			awsSecretKey,
-			awsSessionToken,
-			awsRegion,
-			awsUseCrossRegionInference,
-			awsProfile,
-			awsUseProfile,
-			vertexProjectId,
-			vertexRegion,
-			openAiBaseUrl,
-			openAiApiKey,
-			openAiModelId,
-			openAiCustomModelInfo,
-			openAiUseAzure,
-			ollamaModelId,
-			ollamaBaseUrl,
-			lmStudioModelId,
-			lmStudioBaseUrl,
-			anthropicBaseUrl,
-			geminiApiKey,
-			openAiNativeApiKey,
-			deepSeekApiKey,
-			azureApiVersion,
-			openAiStreamingEnabled,
-			openRouterModelId,
-			openRouterBaseUrl,
-			openRouterModelInfo,
-			openRouterUseMiddleOutTransform,
-			vsCodeLmModelSelector,
-			mistralApiKey,
-			mistralCodestralUrl,
-			unboundApiKey,
-			unboundModelId,
-			unboundModelInfo,
-			requestyApiKey,
-			requestyModelId,
-			requestyModelInfo,
-			modelTemperature,
-			modelMaxTokens,
-			modelMaxThinkingTokens,
-		} = apiConfiguration
-		await Promise.all([
-			this.updateGlobalState("apiProvider", apiProvider),
-			this.updateGlobalState("apiModelId", apiModelId),
-			this.storeSecret("apiKey", apiKey),
-			this.updateGlobalState("glamaModelId", glamaModelId),
-			this.updateGlobalState("glamaModelInfo", glamaModelInfo),
-			this.storeSecret("glamaApiKey", glamaApiKey),
-			this.storeSecret("openRouterApiKey", openRouterApiKey),
-			this.storeSecret("awsAccessKey", awsAccessKey),
-			this.storeSecret("awsSecretKey", awsSecretKey),
-			this.storeSecret("awsSessionToken", awsSessionToken),
-			this.updateGlobalState("awsRegion", awsRegion),
-			this.updateGlobalState("awsUseCrossRegionInference", awsUseCrossRegionInference),
-			this.updateGlobalState("awsProfile", awsProfile),
-			this.updateGlobalState("awsUseProfile", awsUseProfile),
-			this.updateGlobalState("vertexProjectId", vertexProjectId),
-			this.updateGlobalState("vertexRegion", vertexRegion),
-			this.updateGlobalState("openAiBaseUrl", openAiBaseUrl),
-			this.storeSecret("openAiApiKey", openAiApiKey),
-			this.updateGlobalState("openAiModelId", openAiModelId),
-			this.updateGlobalState("openAiCustomModelInfo", openAiCustomModelInfo),
-			this.updateGlobalState("openAiUseAzure", openAiUseAzure),
-			this.updateGlobalState("ollamaModelId", ollamaModelId),
-			this.updateGlobalState("ollamaBaseUrl", ollamaBaseUrl),
-			this.updateGlobalState("lmStudioModelId", lmStudioModelId),
-			this.updateGlobalState("lmStudioBaseUrl", lmStudioBaseUrl),
-			this.updateGlobalState("anthropicBaseUrl", anthropicBaseUrl),
-			this.storeSecret("geminiApiKey", geminiApiKey),
-			this.storeSecret("openAiNativeApiKey", openAiNativeApiKey),
-			this.storeSecret("deepSeekApiKey", deepSeekApiKey),
-			this.updateGlobalState("azureApiVersion", azureApiVersion),
-			this.updateGlobalState("openAiStreamingEnabled", openAiStreamingEnabled),
-			this.updateGlobalState("openRouterModelId", openRouterModelId),
-			this.updateGlobalState("openRouterModelInfo", openRouterModelInfo),
-			this.updateGlobalState("openRouterBaseUrl", openRouterBaseUrl),
-			this.updateGlobalState("openRouterUseMiddleOutTransform", openRouterUseMiddleOutTransform),
-			this.updateGlobalState("vsCodeLmModelSelector", vsCodeLmModelSelector),
-			this.storeSecret("mistralApiKey", mistralApiKey),
-			this.updateGlobalState("mistralCodestralUrl", mistralCodestralUrl),
-			this.storeSecret("unboundApiKey", unboundApiKey),
-			this.updateGlobalState("unboundModelId", unboundModelId),
-			this.updateGlobalState("unboundModelInfo", unboundModelInfo),
-			this.storeSecret("requestyApiKey", requestyApiKey),
-			this.updateGlobalState("requestyModelId", requestyModelId),
-			this.updateGlobalState("requestyModelInfo", requestyModelInfo),
-			this.updateGlobalState("modelTemperature", modelTemperature),
-			this.updateGlobalState("modelMaxTokens", modelMaxTokens),
-			this.updateGlobalState("anthropicThinking", modelMaxThinkingTokens),
-		])
+		// Update all configuration values through the contextProxy
+		await this.contextProxy.updateApiConfiguration(apiConfiguration)
+
 		if (this.cline) {
 			this.cline.api = buildApiHandler(apiConfiguration)
 		}
@@ -1815,13 +1751,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	}
 
 	async ensureSettingsDirectoryExists(): Promise<string> {
-		const settingsDir = path.join(this.context.globalStorageUri.fsPath, "settings")
+		const settingsDir = path.join(this.contextProxy.globalStorageUri.fsPath, "settings")
 		await fs.mkdir(settingsDir, { recursive: true })
 		return settingsDir
 	}
 
 	private async ensureCacheDirectoryExists() {
-		const cacheDir = path.join(this.context.globalStorageUri.fsPath, "cache")
+		const cacheDir = path.join(this.contextProxy.globalStorageUri.fsPath, "cache")
 		await fs.mkdir(cacheDir, { recursive: true })
 		return cacheDir
 	}
@@ -1909,7 +1845,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		const history = ((await this.getGlobalState("taskHistory")) as HistoryItem[] | undefined) || []
 		const historyItem = history.find((item) => item.id === id)
 		if (historyItem) {
-			const taskDirPath = path.join(this.context.globalStorageUri.fsPath, "tasks", id)
+			const taskDirPath = path.join(this.contextProxy.globalStorageUri.fsPath, "tasks", id)
 			const apiConversationHistoryFilePath = path.join(taskDirPath, GlobalFileNames.apiConversationHistory)
 			const uiMessagesFilePath = path.join(taskDirPath, GlobalFileNames.uiMessages)
 			const fileExists = await fileExistsAtPath(apiConversationHistoryFilePath)
@@ -1972,21 +1908,8 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			await fs.unlink(legacyMessagesFilePath)
 		}
 
-		const { enableCheckpoints } = await this.getState()
-		const baseDir = vscode.workspace.workspaceFolders?.map((folder) => folder.uri.fsPath).at(0)
-
-		// Delete checkpoints branch.
-		if (enableCheckpoints && baseDir) {
-			const branchSummary = await simpleGit(baseDir)
-				.branch(["-D", `roo-code-checkpoints-${id}`])
-				.catch(() => undefined)
-
-			if (branchSummary) {
-				console.log(`[deleteTaskWithId${id}] deleted checkpoints branch`)
-			}
-		}
-
-		// Delete checkpoints directory
+		// Delete checkpoints directory.
+		// TODO: Also delete the workspace branch if it exists.
 		const checkpointsDir = path.join(taskDirPath, "checkpoints")
 
 		if (await fileExistsAtPath(checkpointsDir)) {
@@ -2033,6 +1956,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundEnabled,
 			diffEnabled,
 			enableCheckpoints,
+			checkpointStorage,
 			taskHistory,
 			soundVolume,
 			browserViewportSize,
@@ -2074,7 +1998,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
 			uriScheme: vscode.env.uriScheme,
 			currentTaskItem: this.cline?.taskId
-				? (taskHistory || []).find((item) => item.id === this.cline?.taskId)
+				? (taskHistory || []).find((item: HistoryItem) => item.id === this.cline?.taskId)
 				: undefined,
 			clineMessages: this.cline?.clineMessages || [],
 			taskHistory: (taskHistory || [])
@@ -2083,6 +2007,7 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			soundEnabled: soundEnabled ?? false,
 			diffEnabled: diffEnabled ?? true,
 			enableCheckpoints: enableCheckpoints ?? true,
+			checkpointStorage: checkpointStorage ?? "task",
 			shouldShowAnnouncement: lastShownAnnouncementId !== this.latestAnnouncementId,
 			allowedCommands,
 			soundVolume: soundVolume ?? 0.5,
@@ -2165,264 +2090,58 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	*/
 
 	async getState() {
-		const [
-			storedApiProvider,
-			apiModelId,
-			apiKey,
-			glamaApiKey,
-			glamaModelId,
-			glamaModelInfo,
-			openRouterApiKey,
-			awsAccessKey,
-			awsSecretKey,
-			awsSessionToken,
-			awsRegion,
-			awsUseCrossRegionInference,
-			awsProfile,
-			awsUseProfile,
-			vertexProjectId,
-			vertexRegion,
-			openAiBaseUrl,
-			openAiApiKey,
-			openAiModelId,
-			openAiCustomModelInfo,
-			openAiUseAzure,
-			ollamaModelId,
-			ollamaBaseUrl,
-			lmStudioModelId,
-			lmStudioBaseUrl,
-			anthropicBaseUrl,
-			geminiApiKey,
-			openAiNativeApiKey,
-			deepSeekApiKey,
-			mistralApiKey,
-			mistralCodestralUrl,
-			azureApiVersion,
-			openAiStreamingEnabled,
-			openRouterModelId,
-			openRouterModelInfo,
-			openRouterBaseUrl,
-			openRouterUseMiddleOutTransform,
-			lastShownAnnouncementId,
-			customInstructions,
-			alwaysAllowReadOnly,
-			alwaysAllowWrite,
-			alwaysAllowExecute,
-			alwaysAllowBrowser,
-			alwaysAllowMcp,
-			alwaysAllowModeSwitch,
-			taskHistory,
-			allowedCommands,
-			soundEnabled,
-			diffEnabled,
-			enableCheckpoints,
-			soundVolume,
-			browserViewportSize,
-			fuzzyMatchThreshold,
-			preferredLanguage,
-			writeDelayMs,
-			screenshotQuality,
-			terminalOutputLineLimit,
-			mcpEnabled,
-			enableMcpServerCreation,
-			alwaysApproveResubmit,
-			requestDelaySeconds,
-			rateLimitSeconds,
-			currentApiConfigName,
-			listApiConfigMeta,
-			vsCodeLmModelSelector,
-			mode,
-			modeApiConfigs,
-			customModePrompts,
-			customSupportPrompts,
-			enhancementApiConfigId,
-			autoApprovalEnabled,
-			customModes,
-			experiments,
-			unboundApiKey,
-			unboundModelId,
-			unboundModelInfo,
-			requestyApiKey,
-			requestyModelId,
-			requestyModelInfo,
-			modelTemperature,
-			modelMaxTokens,
-			modelMaxThinkingTokens,
-			maxOpenTabsContext,
-			browserToolEnabled,
-		] = await Promise.all([
-			this.getGlobalState("apiProvider") as Promise<ApiProvider | undefined>,
-			this.getGlobalState("apiModelId") as Promise<string | undefined>,
-			this.getSecret("apiKey") as Promise<string | undefined>,
-			this.getSecret("glamaApiKey") as Promise<string | undefined>,
-			this.getGlobalState("glamaModelId") as Promise<string | undefined>,
-			this.getGlobalState("glamaModelInfo") as Promise<ModelInfo | undefined>,
-			this.getSecret("openRouterApiKey") as Promise<string | undefined>,
-			this.getSecret("awsAccessKey") as Promise<string | undefined>,
-			this.getSecret("awsSecretKey") as Promise<string | undefined>,
-			this.getSecret("awsSessionToken") as Promise<string | undefined>,
-			this.getGlobalState("awsRegion") as Promise<string | undefined>,
-			this.getGlobalState("awsUseCrossRegionInference") as Promise<boolean | undefined>,
-			this.getGlobalState("awsProfile") as Promise<string | undefined>,
-			this.getGlobalState("awsUseProfile") as Promise<boolean | undefined>,
-			this.getGlobalState("vertexProjectId") as Promise<string | undefined>,
-			this.getGlobalState("vertexRegion") as Promise<string | undefined>,
-			this.getGlobalState("openAiBaseUrl") as Promise<string | undefined>,
-			this.getSecret("openAiApiKey") as Promise<string | undefined>,
-			this.getGlobalState("openAiModelId") as Promise<string | undefined>,
-			this.getGlobalState("openAiCustomModelInfo") as Promise<ModelInfo | undefined>,
-			this.getGlobalState("openAiUseAzure") as Promise<boolean | undefined>,
-			this.getGlobalState("ollamaModelId") as Promise<string | undefined>,
-			this.getGlobalState("ollamaBaseUrl") as Promise<string | undefined>,
-			this.getGlobalState("lmStudioModelId") as Promise<string | undefined>,
-			this.getGlobalState("lmStudioBaseUrl") as Promise<string | undefined>,
-			this.getGlobalState("anthropicBaseUrl") as Promise<string | undefined>,
-			this.getSecret("geminiApiKey") as Promise<string | undefined>,
-			this.getSecret("openAiNativeApiKey") as Promise<string | undefined>,
-			this.getSecret("deepSeekApiKey") as Promise<string | undefined>,
-			this.getSecret("mistralApiKey") as Promise<string | undefined>,
-			this.getGlobalState("mistralCodestralUrl") as Promise<string | undefined>,
-			this.getGlobalState("azureApiVersion") as Promise<string | undefined>,
-			this.getGlobalState("openAiStreamingEnabled") as Promise<boolean | undefined>,
-			this.getGlobalState("openRouterModelId") as Promise<string | undefined>,
-			this.getGlobalState("openRouterModelInfo") as Promise<ModelInfo | undefined>,
-			this.getGlobalState("openRouterBaseUrl") as Promise<string | undefined>,
-			this.getGlobalState("openRouterUseMiddleOutTransform") as Promise<boolean | undefined>,
-			this.getGlobalState("lastShownAnnouncementId") as Promise<string | undefined>,
-			this.getGlobalState("customInstructions") as Promise<string | undefined>,
-			this.getGlobalState("alwaysAllowReadOnly") as Promise<boolean | undefined>,
-			this.getGlobalState("alwaysAllowWrite") as Promise<boolean | undefined>,
-			this.getGlobalState("alwaysAllowExecute") as Promise<boolean | undefined>,
-			this.getGlobalState("alwaysAllowBrowser") as Promise<boolean | undefined>,
-			this.getGlobalState("alwaysAllowMcp") as Promise<boolean | undefined>,
-			this.getGlobalState("alwaysAllowModeSwitch") as Promise<boolean | undefined>,
-			this.getGlobalState("taskHistory") as Promise<HistoryItem[] | undefined>,
-			this.getGlobalState("allowedCommands") as Promise<string[] | undefined>,
-			this.getGlobalState("soundEnabled") as Promise<boolean | undefined>,
-			this.getGlobalState("diffEnabled") as Promise<boolean | undefined>,
-			this.getGlobalState("enableCheckpoints") as Promise<boolean | undefined>,
-			this.getGlobalState("soundVolume") as Promise<number | undefined>,
-			this.getGlobalState("browserViewportSize") as Promise<string | undefined>,
-			this.getGlobalState("fuzzyMatchThreshold") as Promise<number | undefined>,
-			this.getGlobalState("preferredLanguage") as Promise<string | undefined>,
-			this.getGlobalState("writeDelayMs") as Promise<number | undefined>,
-			this.getGlobalState("screenshotQuality") as Promise<number | undefined>,
-			this.getGlobalState("terminalOutputLineLimit") as Promise<number | undefined>,
-			this.getGlobalState("mcpEnabled") as Promise<boolean | undefined>,
-			this.getGlobalState("enableMcpServerCreation") as Promise<boolean | undefined>,
-			this.getGlobalState("alwaysApproveResubmit") as Promise<boolean | undefined>,
-			this.getGlobalState("requestDelaySeconds") as Promise<number | undefined>,
-			this.getGlobalState("rateLimitSeconds") as Promise<number | undefined>,
-			this.getGlobalState("currentApiConfigName") as Promise<string | undefined>,
-			this.getGlobalState("listApiConfigMeta") as Promise<ApiConfigMeta[] | undefined>,
-			this.getGlobalState("vsCodeLmModelSelector") as Promise<vscode.LanguageModelChatSelector | undefined>,
-			this.getGlobalState("mode") as Promise<Mode | undefined>,
-			this.getGlobalState("modeApiConfigs") as Promise<Record<Mode, string> | undefined>,
-			this.getGlobalState("customModePrompts") as Promise<CustomModePrompts | undefined>,
-			this.getGlobalState("customSupportPrompts") as Promise<CustomSupportPrompts | undefined>,
-			this.getGlobalState("enhancementApiConfigId") as Promise<string | undefined>,
-			this.getGlobalState("autoApprovalEnabled") as Promise<boolean | undefined>,
-			this.customModesManager.getCustomModes(),
-			this.getGlobalState("experiments") as Promise<Record<ExperimentId, boolean> | undefined>,
-			this.getSecret("unboundApiKey") as Promise<string | undefined>,
-			this.getGlobalState("unboundModelId") as Promise<string | undefined>,
-			this.getGlobalState("unboundModelInfo") as Promise<ModelInfo | undefined>,
-			this.getSecret("requestyApiKey") as Promise<string | undefined>,
-			this.getGlobalState("requestyModelId") as Promise<string | undefined>,
-			this.getGlobalState("requestyModelInfo") as Promise<ModelInfo | undefined>,
-			this.getGlobalState("modelTemperature") as Promise<number | undefined>,
-			this.getGlobalState("modelMaxTokens") as Promise<number | undefined>,
-			this.getGlobalState("anthropicThinking") as Promise<number | undefined>,
-			this.getGlobalState("maxOpenTabsContext") as Promise<number | undefined>,
-			this.getGlobalState("browserToolEnabled") as Promise<boolean | undefined>,
-		])
+		// Get ApiConfiguration directly from contextProxy
+		const apiConfiguration = this.contextProxy.getApiConfiguration()
 
-		let apiProvider: ApiProvider
-		if (storedApiProvider) {
-			apiProvider = storedApiProvider
-		} else {
-			// Either new user or legacy user that doesn't have the apiProvider stored in state
-			// (If they're using OpenRouter or Bedrock, then apiProvider state will exist)
-			if (apiKey) {
-				apiProvider = "anthropic"
-			} else {
-				// New users should default to openrouter
-				apiProvider = "openrouter"
-			}
-		}
+		// Create an object to store all fetched values (excluding API config which we already have)
+		const stateValues: Record<GlobalStateKey, any> = {} as Record<GlobalStateKey, any>
+
+		// Create promise arrays for global state
+		const statePromises = GLOBAL_STATE_KEYS
+			// Filter out API config keys since we already have them
+			.filter((key) => !API_CONFIG_KEYS.includes(key))
+			.map((key) => this.getGlobalState(key))
 
+		// Add promise for custom modes which is handled separately
+		const customModesPromise = this.customModesManager.getCustomModes()
+
+		let idx = 0
+		const valuePromises = await Promise.all([...statePromises, customModesPromise])
+
+		// Populate stateValues
+		GLOBAL_STATE_KEYS.filter((key) => !API_CONFIG_KEYS.includes(key)).forEach((key) => {
+			stateValues[key] = valuePromises[idx]
+			idx = idx + 1
+		})
+
+		let customModes = valuePromises[idx] as ModeConfig[] | undefined
+
+		// Return the same structure as before
 		return {
-			apiConfiguration: {
-				apiProvider,
-				apiModelId,
-				apiKey,
-				glamaApiKey,
-				glamaModelId,
-				glamaModelInfo,
-				openRouterApiKey,
-				awsAccessKey,
-				awsSecretKey,
-				awsSessionToken,
-				awsRegion,
-				awsUseCrossRegionInference,
-				awsProfile,
-				awsUseProfile,
-				vertexProjectId,
-				vertexRegion,
-				openAiBaseUrl,
-				openAiApiKey,
-				openAiModelId,
-				openAiCustomModelInfo,
-				openAiUseAzure,
-				ollamaModelId,
-				ollamaBaseUrl,
-				lmStudioModelId,
-				lmStudioBaseUrl,
-				anthropicBaseUrl,
-				geminiApiKey,
-				openAiNativeApiKey,
-				deepSeekApiKey,
-				mistralApiKey,
-				mistralCodestralUrl,
-				azureApiVersion,
-				openAiStreamingEnabled,
-				openRouterModelId,
-				openRouterModelInfo,
-				openRouterBaseUrl,
-				openRouterUseMiddleOutTransform,
-				vsCodeLmModelSelector,
-				unboundApiKey,
-				unboundModelId,
-				unboundModelInfo,
-				requestyApiKey,
-				requestyModelId,
-				requestyModelInfo,
-				modelTemperature,
-				modelMaxTokens,
-				modelMaxThinkingTokens,
-			},
-			lastShownAnnouncementId,
-			customInstructions,
-			alwaysAllowReadOnly: alwaysAllowReadOnly ?? false,
-			alwaysAllowWrite: alwaysAllowWrite ?? false,
-			alwaysAllowExecute: alwaysAllowExecute ?? false,
-			alwaysAllowBrowser: alwaysAllowBrowser ?? false,
-			alwaysAllowMcp: alwaysAllowMcp ?? false,
-			alwaysAllowModeSwitch: alwaysAllowModeSwitch ?? false,
-			taskHistory,
-			allowedCommands,
-			soundEnabled: soundEnabled ?? false,
-			diffEnabled: diffEnabled ?? true,
-			enableCheckpoints: enableCheckpoints ?? true,
-			soundVolume,
-			browserViewportSize: browserViewportSize ?? "900x600",
-			screenshotQuality: screenshotQuality ?? 75,
-			fuzzyMatchThreshold: fuzzyMatchThreshold ?? 1.0,
-			writeDelayMs: writeDelayMs ?? 1000,
-			terminalOutputLineLimit: terminalOutputLineLimit ?? 500,
-			mode: mode ?? defaultModeSlug,
+			apiConfiguration,
+			lastShownAnnouncementId: stateValues.lastShownAnnouncementId,
+			customInstructions: stateValues.customInstructions,
+			alwaysAllowReadOnly: stateValues.alwaysAllowReadOnly ?? false,
+			alwaysAllowWrite: stateValues.alwaysAllowWrite ?? false,
+			alwaysAllowExecute: stateValues.alwaysAllowExecute ?? false,
+			alwaysAllowBrowser: stateValues.alwaysAllowBrowser ?? false,
+			alwaysAllowMcp: stateValues.alwaysAllowMcp ?? false,
+			alwaysAllowModeSwitch: stateValues.alwaysAllowModeSwitch ?? false,
+			taskHistory: stateValues.taskHistory,
+			allowedCommands: stateValues.allowedCommands,
+			soundEnabled: stateValues.soundEnabled ?? false,
+			diffEnabled: stateValues.diffEnabled ?? true,
+			enableCheckpoints: stateValues.enableCheckpoints ?? false,
+			checkpointStorage: stateValues.checkpointStorage ?? "task",
+			soundVolume: stateValues.soundVolume,
+			browserViewportSize: stateValues.browserViewportSize ?? "900x600",
+			screenshotQuality: stateValues.screenshotQuality ?? 75,
+			fuzzyMatchThreshold: stateValues.fuzzyMatchThreshold ?? 1.0,
+			writeDelayMs: stateValues.writeDelayMs ?? 1000,
+			terminalOutputLineLimit: stateValues.terminalOutputLineLimit ?? 500,
+			mode: stateValues.mode ?? defaultModeSlug,
 			preferredLanguage:
-				preferredLanguage ??
+				stateValues.preferredLanguage ??
 				(() => {
 					// Get VSCode's locale setting
 					const vscodeLang = vscode.env.language
@@ -2452,23 +2171,23 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 					// Return mapped language or default to English
 					return langMap[vscodeLang] ?? langMap[vscodeLang.split("-")[0]] ?? "English"
 				})(),
-			mcpEnabled: mcpEnabled ?? true,
-			enableMcpServerCreation: enableMcpServerCreation ?? true,
-			alwaysApproveResubmit: alwaysApproveResubmit ?? false,
-			requestDelaySeconds: Math.max(5, requestDelaySeconds ?? 10),
-			rateLimitSeconds: rateLimitSeconds ?? 0,
-			currentApiConfigName: currentApiConfigName ?? "default",
-			listApiConfigMeta: listApiConfigMeta ?? [],
-			modeApiConfigs: modeApiConfigs ?? ({} as Record<Mode, string>),
-			customModePrompts: customModePrompts ?? {},
-			customSupportPrompts: customSupportPrompts ?? {},
-			enhancementApiConfigId,
-			experiments: experiments ?? experimentDefault,
-			autoApprovalEnabled: autoApprovalEnabled ?? false,
+			mcpEnabled: stateValues.mcpEnabled ?? true,
+			enableMcpServerCreation: stateValues.enableMcpServerCreation ?? true,
+			alwaysApproveResubmit: stateValues.alwaysApproveResubmit ?? false,
+			requestDelaySeconds: Math.max(5, stateValues.requestDelaySeconds ?? 10),
+			rateLimitSeconds: stateValues.rateLimitSeconds ?? 0,
+			currentApiConfigName: stateValues.currentApiConfigName ?? "default",
+			listApiConfigMeta: stateValues.listApiConfigMeta ?? [],
+			modeApiConfigs: stateValues.modeApiConfigs ?? ({} as Record<Mode, string>),
+			customModePrompts: stateValues.customModePrompts ?? {},
+			customSupportPrompts: stateValues.customSupportPrompts ?? {},
+			enhancementApiConfigId: stateValues.enhancementApiConfigId,
+			experiments: stateValues.experiments ?? experimentDefault,
+			autoApprovalEnabled: stateValues.autoApprovalEnabled ?? false,
 			customModes,
-			maxOpenTabsContext: maxOpenTabsContext ?? 20,
-			openRouterUseMiddleOutTransform: openRouterUseMiddleOutTransform ?? true,
-			browserToolEnabled: browserToolEnabled ?? true,
+			maxOpenTabsContext: stateValues.maxOpenTabsContext ?? 20,
+			openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform ?? true,
+			browserToolEnabled: stateValues.browserToolEnabled ?? true,
 		}
 	}
 
@@ -2488,25 +2207,21 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	// global
 
 	async updateGlobalState(key: GlobalStateKey, value: any) {
-		await this.context.globalState.update(key, value)
+		await this.contextProxy.updateGlobalState(key, value)
 	}
 
 	async getGlobalState(key: GlobalStateKey) {
-		return await this.context.globalState.get(key)
+		return await this.contextProxy.getGlobalState(key)
 	}
 
 	// secrets
 
 	public async storeSecret(key: SecretKey, value?: string) {
-		if (value) {
-			await this.context.secrets.store(key, value)
-		} else {
-			await this.context.secrets.delete(key)
-		}
+		await this.contextProxy.storeSecret(key, value)
 	}
 
 	private async getSecret(key: SecretKey) {
-		return await this.context.secrets.get(key)
+		return await this.contextProxy.getSecret(key)
 	}
 
 	// dev
@@ -2523,26 +2238,13 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		}
 
 		for (const key of this.context.globalState.keys()) {
-			await this.context.globalState.update(key, undefined)
+			await this.contextProxy.updateGlobalState(key, undefined)
 		}
-		const secretKeys: SecretKey[] = [
-			"apiKey",
-			"glamaApiKey",
-			"openRouterApiKey",
-			"awsAccessKey",
-			"awsSecretKey",
-			"awsSessionToken",
-			"openAiApiKey",
-			"geminiApiKey",
-			"openAiNativeApiKey",
-			"deepSeekApiKey",
-			"mistralApiKey",
-			"unboundApiKey",
-			"requestyApiKey",
-		]
-		for (const key of secretKeys) {
+
+		for (const key of SECRET_KEYS) {
 			await this.storeSecret(key, undefined)
 		}
+
 		await this.configManager.resetAllConfigs()
 		await this.customModesManager.resetCustomModes()
 		if (this.cline) {

+ 158 - 0
src/core/webview/__tests__/ClineProvider.test.ts

@@ -5,6 +5,7 @@ import axios from "axios"
 
 import { ClineProvider } from "../ClineProvider"
 import { ExtensionMessage, ExtensionState } from "../../../shared/ExtensionMessage"
+import { GlobalStateKey, SecretKey } from "../../../shared/globalState"
 import { setSoundEnabled } from "../../../utils/sound"
 import { defaultModeSlug } from "../../../shared/modes"
 import { experimentDefault } from "../../../shared/experiments"
@@ -12,6 +13,49 @@ import { experimentDefault } from "../../../shared/experiments"
 // Mock setup must come before imports
 jest.mock("../../prompts/sections/custom-instructions")
 
+// Mock ContextProxy
+jest.mock("../../contextProxy", () => {
+	return {
+		ContextProxy: jest.fn().mockImplementation((context) => ({
+			originalContext: context,
+			extensionUri: context.extensionUri,
+			extensionPath: context.extensionPath,
+			globalStorageUri: context.globalStorageUri,
+			logUri: context.logUri,
+			extension: context.extension,
+			extensionMode: context.extensionMode,
+			getGlobalState: jest
+				.fn()
+				.mockImplementation((key, defaultValue) => context.globalState.get(key, defaultValue)),
+			updateGlobalState: jest.fn().mockImplementation((key, value) => context.globalState.update(key, value)),
+			getSecret: jest.fn().mockImplementation((key) => context.secrets.get(key)),
+			storeSecret: jest
+				.fn()
+				.mockImplementation((key, value) =>
+					value ? context.secrets.store(key, value) : context.secrets.delete(key),
+				),
+			getApiConfiguration: jest.fn().mockImplementation(() => ({
+				apiProvider: "openrouter",
+				// Add other common properties
+			})),
+			updateApiConfiguration: jest.fn().mockImplementation(async (apiConfiguration) => {
+				// Mock implementation that simulates updating state and secrets
+				for (const [key, value] of Object.entries(apiConfiguration)) {
+					if (key === "apiKey" || key === "openAiApiKey") {
+						context.secrets.store(key, value)
+					} else {
+						context.globalState.update(key, value)
+					}
+				}
+				return Promise.resolve()
+			}),
+			saveChanges: jest.fn().mockResolvedValue(undefined),
+			dispose: jest.fn().mockResolvedValue(undefined),
+			hasPendingChanges: jest.fn().mockReturnValue(false),
+		})),
+	}
+})
+
 // Mock dependencies
 jest.mock("vscode")
 jest.mock("delay")
@@ -235,6 +279,12 @@ describe("ClineProvider", () => {
 	let mockOutputChannel: vscode.OutputChannel
 	let mockWebviewView: vscode.WebviewView
 	let mockPostMessage: jest.Mock
+	let mockContextProxy: {
+		updateGlobalState: jest.Mock
+		getGlobalState: jest.Mock
+		storeSecret: jest.Mock
+		dispose: jest.Mock
+	}
 
 	beforeEach(() => {
 		// Reset mocks
@@ -307,6 +357,8 @@ describe("ClineProvider", () => {
 		} as unknown as vscode.WebviewView
 
 		provider = new ClineProvider(mockContext, mockOutputChannel)
+		// @ts-ignore - Access private property for testing
+		mockContextProxy = provider.contextProxy
 
 		// @ts-ignore - Accessing private property for testing.
 		provider.customModesManager = mockCustomModesManager
@@ -370,6 +422,7 @@ describe("ClineProvider", () => {
 			soundEnabled: false,
 			diffEnabled: false,
 			enableCheckpoints: false,
+			checkpointStorage: "task",
 			writeDelayMs: 1000,
 			browserViewportSize: "900x600",
 			fuzzyMatchThreshold: 1.0,
@@ -478,6 +531,7 @@ describe("ClineProvider", () => {
 
 		await messageHandler({ type: "writeDelayMs", value: 2000 })
 
+		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("writeDelayMs", 2000)
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("writeDelayMs", 2000)
 		expect(mockPostMessage).toHaveBeenCalled()
 	})
@@ -491,6 +545,7 @@ describe("ClineProvider", () => {
 		// Simulate setting sound to enabled
 		await messageHandler({ type: "soundEnabled", bool: true })
 		expect(setSoundEnabled).toHaveBeenCalledWith(true)
+		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("soundEnabled", true)
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("soundEnabled", true)
 		expect(mockPostMessage).toHaveBeenCalled()
 
@@ -613,6 +668,7 @@ describe("ClineProvider", () => {
 
 		// Test alwaysApproveResubmit
 		await messageHandler({ type: "alwaysApproveResubmit", bool: true })
+		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("alwaysApproveResubmit", true)
 		expect(mockContext.globalState.update).toHaveBeenCalledWith("alwaysApproveResubmit", true)
 		expect(mockPostMessage).toHaveBeenCalled()
 
@@ -694,6 +750,7 @@ describe("ClineProvider", () => {
 			mode: "code",
 			diffEnabled: true,
 			enableCheckpoints: false,
+			checkpointStorage: "task",
 			fuzzyMatchThreshold: 1.0,
 			experiments: experimentDefault,
 		} as any)
@@ -712,6 +769,7 @@ describe("ClineProvider", () => {
 			customInstructions: modeCustomInstructions,
 			enableDiff: true,
 			enableCheckpoints: false,
+			checkpointStorage: "task",
 			fuzzyMatchThreshold: 1.0,
 			task: "Test task",
 			experiments: experimentDefault,
@@ -1481,6 +1539,106 @@ describe("ClineProvider", () => {
 			expect(mockContext.globalState.update).toHaveBeenCalledWith("listApiConfigMeta", [
 				{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
 			])
+			expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("listApiConfigMeta", [
+				{ name: "test-config", id: "test-id", apiProvider: "anthropic" },
+			])
 		})
 	})
 })
+
+describe("ContextProxy integration", () => {
+	let provider: ClineProvider
+	let mockContext: vscode.ExtensionContext
+	let mockOutputChannel: vscode.OutputChannel
+	let mockContextProxy: any
+
+	beforeEach(() => {
+		// Reset mocks
+		jest.clearAllMocks()
+
+		// Setup basic mocks
+		mockContext = {
+			globalState: { get: jest.fn(), update: jest.fn(), keys: jest.fn().mockReturnValue([]) },
+			secrets: { get: jest.fn(), store: jest.fn(), delete: jest.fn() },
+			extensionUri: {} as vscode.Uri,
+			globalStorageUri: { fsPath: "/test/path" },
+			extension: { packageJSON: { version: "1.0.0" } },
+		} as unknown as vscode.ExtensionContext
+
+		mockOutputChannel = { appendLine: jest.fn() } as unknown as vscode.OutputChannel
+		provider = new ClineProvider(mockContext, mockOutputChannel)
+
+		// @ts-ignore - accessing private property for testing
+		mockContextProxy = provider.contextProxy
+	})
+
+	test("updateGlobalState uses contextProxy", async () => {
+		await provider.updateGlobalState("currentApiConfigName" as GlobalStateKey, "testValue")
+		expect(mockContextProxy.updateGlobalState).toHaveBeenCalledWith("currentApiConfigName", "testValue")
+	})
+
+	test("getGlobalState uses contextProxy", async () => {
+		mockContextProxy.getGlobalState.mockResolvedValueOnce("testValue")
+		const result = await provider.getGlobalState("currentApiConfigName" as GlobalStateKey)
+		expect(mockContextProxy.getGlobalState).toHaveBeenCalledWith("currentApiConfigName")
+		expect(result).toBe("testValue")
+	})
+
+	test("storeSecret uses contextProxy", async () => {
+		await provider.storeSecret("apiKey" as SecretKey, "test-secret")
+		expect(mockContextProxy.storeSecret).toHaveBeenCalledWith("apiKey", "test-secret")
+	})
+
+	test("contextProxy methods are available", () => {
+		// Verify the contextProxy has all the required methods
+		expect(mockContextProxy.getGlobalState).toBeDefined()
+		expect(mockContextProxy.updateGlobalState).toBeDefined()
+		expect(mockContextProxy.storeSecret).toBeDefined()
+		expect(mockContextProxy.getApiConfiguration).toBeDefined()
+		expect(mockContextProxy.updateApiConfiguration).toBeDefined()
+	})
+
+	test("getState uses contextProxy.getApiConfiguration", async () => {
+		// Setup mock API configuration
+		const mockApiConfig = {
+			apiProvider: "anthropic",
+			apiModelId: "claude-latest",
+			apiKey: "test-api-key",
+		}
+		mockContextProxy.getApiConfiguration.mockReturnValue(mockApiConfig)
+
+		// Get state
+		const state = await provider.getState()
+
+		// Verify getApiConfiguration was called
+		expect(mockContextProxy.getApiConfiguration).toHaveBeenCalled()
+		// Verify state has the API configuration from contextProxy
+		expect(state.apiConfiguration).toBe(mockApiConfig)
+	})
+
+	test("updateApiConfiguration uses contextProxy.updateApiConfiguration", async () => {
+		// Setup test config
+		const testApiConfig = {
+			apiProvider: "anthropic",
+			apiModelId: "claude-latest",
+			apiKey: "test-api-key",
+		}
+
+		// Mock methods needed for the test
+		provider.configManager = {
+			listConfig: jest.fn().mockResolvedValue([]),
+			setModeConfig: jest.fn(),
+		} as any
+
+		// Mock getState for mode
+		jest.spyOn(provider, "getState").mockResolvedValue({
+			mode: "code",
+		} as any)
+
+		// Call the private method - need to use any to access it
+		await (provider as any).updateApiConfiguration(testApiConfig)
+
+		// Verify contextProxy.updateApiConfiguration was called with the right config
+		expect(mockContextProxy.updateApiConfiguration).toHaveBeenCalledWith(testApiConfig)
+	})
+})

+ 2 - 3
src/extension.ts

@@ -5,7 +5,7 @@ import { createClineAPI } from "./exports"
 import "./utils/path" // Necessary to have access to String.prototype.toPosix.
 import { CodeActionProvider } from "./core/CodeActionProvider"
 import { DIFF_VIEW_URI_SCHEME } from "./integrations/editor/DiffViewProvider"
-import { handleUri, registerCommands, registerCodeActions, registerTerminalActions } from "./activate"
+import { handleUri, registerCommands, registerCodeActions } from "./activate"
 import { McpServerManager } from "./services/mcp/McpServerManager"
 
 /**
@@ -127,12 +127,11 @@ export function activate(context: vscode.ExtensionContext) {
 	)
 
 	registerCodeActions(context)
-	registerTerminalActions(context)
 
 	return createClineAPI(outputChannel, sidebarProvider)
 }
 
-// This method is called when your extension is deactivated
+// This method is called when your extension is deactivated.
 export async function deactivate() {
 	outputChannel.appendLine("Roo-Code extension deactivated")
 	// Clean up MCP server manager

+ 2 - 0
src/shared/ExtensionMessage.ts

@@ -7,6 +7,7 @@ import { GitCommit } from "../utils/git"
 import { Mode, CustomModePrompts, ModeConfig } from "./modes"
 import { CustomSupportPrompts } from "./support-prompt"
 import { ExperimentId } from "./experiments"
+import { CheckpointStorage } from "./checkpoints"
 
 export interface LanguageModelChatSelector {
 	vendor?: string
@@ -117,6 +118,7 @@ export interface ExtensionState {
 	soundVolume?: number
 	diffEnabled?: boolean
 	enableCheckpoints: boolean
+	checkpointStorage: CheckpointStorage
 	browserViewportSize?: string
 	screenshotQuality?: number
 	fuzzyMatchThreshold?: number

+ 1 - 0
src/shared/WebviewMessage.ts

@@ -53,6 +53,7 @@ export interface WebviewMessage {
 		| "soundVolume"
 		| "diffEnabled"
 		| "enableCheckpoints"
+		| "checkpointStorage"
 		| "browserViewportSize"
 		| "screenshotQuality"
 		| "openMcpSettings"

+ 48 - 1
src/shared/api.ts

@@ -50,6 +50,8 @@ export interface ApiHandlerOptions {
 	ollamaBaseUrl?: string
 	lmStudioModelId?: string
 	lmStudioBaseUrl?: string
+	lmStudioDraftModelId?: string
+	lmStudioSpeculativeDecodingEnabled?: boolean
 	geminiApiKey?: string
 	openAiNativeApiKey?: string
 	mistralApiKey?: string
@@ -57,7 +59,6 @@ export interface ApiHandlerOptions {
 	azureApiVersion?: string
 	openRouterUseMiddleOutTransform?: boolean
 	openAiStreamingEnabled?: boolean
-	setAzureApiVersion?: boolean
 	deepSeekBaseUrl?: string
 	deepSeekApiKey?: string
 	includeMaxTokens?: boolean
@@ -77,6 +78,52 @@ export type ApiConfiguration = ApiHandlerOptions & {
 	id?: string // stable unique identifier
 }
 
+// Import GlobalStateKey type from globalState.ts
+import { GlobalStateKey } from "./globalState"
+
+// Define API configuration keys for dynamic object building
+export const API_CONFIG_KEYS: GlobalStateKey[] = [
+	"apiModelId",
+	"anthropicBaseUrl",
+	"vsCodeLmModelSelector",
+	"glamaModelId",
+	"glamaModelInfo",
+	"openRouterModelId",
+	"openRouterModelInfo",
+	"openRouterBaseUrl",
+	"awsRegion",
+	"awsUseCrossRegionInference",
+	// "awsUsePromptCache", // NOT exist on GlobalStateKey
+	// "awspromptCacheId", // NOT exist on GlobalStateKey
+	"awsProfile",
+	"awsUseProfile",
+	"vertexProjectId",
+	"vertexRegion",
+	"openAiBaseUrl",
+	"openAiModelId",
+	"openAiCustomModelInfo",
+	"openAiUseAzure",
+	"ollamaModelId",
+	"ollamaBaseUrl",
+	"lmStudioModelId",
+	"lmStudioBaseUrl",
+	"lmStudioDraftModelId",
+	"lmStudioSpeculativeDecodingEnabled",
+	"mistralCodestralUrl",
+	"azureApiVersion",
+	"openRouterUseMiddleOutTransform",
+	"openAiStreamingEnabled",
+	// "deepSeekBaseUrl", //  not exist on GlobalStateKey
+	// "includeMaxTokens", // not exist on GlobalStateKey
+	"unboundModelId",
+	"unboundModelInfo",
+	"requestyModelId",
+	"requestyModelInfo",
+	"modelTemperature",
+	"modelMaxTokens",
+	"modelMaxThinkingTokens",
+]
+
 // Models
 
 export interface ModelInfo {

+ 16 - 19
src/shared/checkExistApiConfig.ts

@@ -1,23 +1,20 @@
 import { ApiConfiguration } from "../shared/api"
+import { SECRET_KEYS } from "./globalState"
 
 export function checkExistKey(config: ApiConfiguration | undefined) {
-	return config
-		? [
-				config.apiKey,
-				config.glamaApiKey,
-				config.openRouterApiKey,
-				config.awsRegion,
-				config.vertexProjectId,
-				config.openAiApiKey,
-				config.ollamaModelId,
-				config.lmStudioModelId,
-				config.geminiApiKey,
-				config.openAiNativeApiKey,
-				config.deepSeekApiKey,
-				config.mistralApiKey,
-				config.vsCodeLmModelSelector,
-				config.requestyApiKey,
-				config.unboundApiKey,
-			].some((key) => key !== undefined)
-		: false
+	if (!config) return false
+
+	// Check all secret keys from the centralized SECRET_KEYS array
+	const hasSecretKey = SECRET_KEYS.some((key) => config[key as keyof ApiConfiguration] !== undefined)
+
+	// Check additional non-secret configuration properties
+	const hasOtherConfig = [
+		config.awsRegion,
+		config.vertexProjectId,
+		config.ollamaModelId,
+		config.lmStudioModelId,
+		config.vsCodeLmModelSelector,
+	].some((value) => value !== undefined)
+
+	return hasSecretKey || hasOtherConfig
 }

+ 5 - 0
src/shared/checkpoints.ts

@@ -0,0 +1,5 @@
+export type CheckpointStorage = "task" | "workspace"
+
+export const isCheckpointStorage = (value: string): value is CheckpointStorage => {
+	return value === "task" || value === "workspace"
+}

+ 99 - 86
src/shared/globalState.ts

@@ -1,87 +1,100 @@
-export type SecretKey =
-	| "apiKey"
-	| "glamaApiKey"
-	| "openRouterApiKey"
-	| "awsAccessKey"
-	| "awsSecretKey"
-	| "awsSessionToken"
-	| "openAiApiKey"
-	| "geminiApiKey"
-	| "openAiNativeApiKey"
-	| "deepSeekApiKey"
-	| "mistralApiKey"
-	| "unboundApiKey"
-	| "requestyApiKey"
+// Define the array first with 'as const' to create a readonly tuple type
+export const SECRET_KEYS = [
+	"apiKey",
+	"glamaApiKey",
+	"openRouterApiKey",
+	"awsAccessKey",
+	"awsSecretKey",
+	"awsSessionToken",
+	"openAiApiKey",
+	"geminiApiKey",
+	"openAiNativeApiKey",
+	"deepSeekApiKey",
+	"mistralApiKey",
+	"unboundApiKey",
+	"requestyApiKey",
+] as const
 
-export type GlobalStateKey =
-	| "apiProvider"
-	| "apiModelId"
-	| "glamaModelId"
-	| "glamaModelInfo"
-	| "awsRegion"
-	| "awsUseCrossRegionInference"
-	| "awsProfile"
-	| "awsUseProfile"
-	| "vertexProjectId"
-	| "vertexRegion"
-	| "lastShownAnnouncementId"
-	| "customInstructions"
-	| "alwaysAllowReadOnly"
-	| "alwaysAllowWrite"
-	| "alwaysAllowExecute"
-	| "alwaysAllowBrowser"
-	| "alwaysAllowMcp"
-	| "alwaysAllowModeSwitch"
-	| "taskHistory"
-	| "openAiBaseUrl"
-	| "openAiModelId"
-	| "openAiCustomModelInfo"
-	| "openAiUseAzure"
-	| "ollamaModelId"
-	| "ollamaBaseUrl"
-	| "lmStudioModelId"
-	| "lmStudioBaseUrl"
-	| "anthropicBaseUrl"
-	| "azureApiVersion"
-	| "openAiStreamingEnabled"
-	| "openRouterModelId"
-	| "openRouterModelInfo"
-	| "openRouterBaseUrl"
-	| "openRouterUseMiddleOutTransform"
-	| "allowedCommands"
-	| "soundEnabled"
-	| "soundVolume"
-	| "diffEnabled"
-	| "enableCheckpoints"
-	| "browserViewportSize"
-	| "screenshotQuality"
-	| "fuzzyMatchThreshold"
-	| "preferredLanguage" // Language setting for Cline's communication
-	| "writeDelayMs"
-	| "terminalOutputLineLimit"
-	| "mcpEnabled"
-	| "enableMcpServerCreation"
-	| "alwaysApproveResubmit"
-	| "requestDelaySeconds"
-	| "rateLimitSeconds"
-	| "currentApiConfigName"
-	| "listApiConfigMeta"
-	| "vsCodeLmModelSelector"
-	| "mode"
-	| "modeApiConfigs"
-	| "customModePrompts"
-	| "customSupportPrompts"
-	| "enhancementApiConfigId"
-	| "experiments" // Map of experiment IDs to their enabled state
-	| "autoApprovalEnabled"
-	| "customModes" // Array of custom modes
-	| "unboundModelId"
-	| "requestyModelId"
-	| "requestyModelInfo"
-	| "unboundModelInfo"
-	| "modelTemperature"
-	| "modelMaxTokens"
-	| "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
-	| "mistralCodestralUrl"
-	| "maxOpenTabsContext"
-	| "browserToolEnabled" // Setting to enable/disable the browser tool
+// Derive the type from the array - creates a union of string literals
+export type SecretKey = (typeof SECRET_KEYS)[number]
+
+// Define the array first with 'as const' to create a readonly tuple type
+export const GLOBAL_STATE_KEYS = [
+	"apiProvider",
+	"apiModelId",
+	"glamaModelId",
+	"glamaModelInfo",
+	"awsRegion",
+	"awsUseCrossRegionInference",
+	"awsProfile",
+	"awsUseProfile",
+	"vertexProjectId",
+	"vertexRegion",
+	"lastShownAnnouncementId",
+	"customInstructions",
+	"alwaysAllowReadOnly",
+	"alwaysAllowWrite",
+	"alwaysAllowExecute",
+	"alwaysAllowBrowser",
+	"alwaysAllowMcp",
+	"alwaysAllowModeSwitch",
+	"taskHistory",
+	"openAiBaseUrl",
+	"openAiModelId",
+	"openAiCustomModelInfo",
+	"openAiUseAzure",
+	"ollamaModelId",
+	"ollamaBaseUrl",
+	"lmStudioModelId",
+	"lmStudioBaseUrl",
+	"anthropicBaseUrl",
+	"modelMaxThinkingTokens",
+	"azureApiVersion",
+	"openAiStreamingEnabled",
+	"openRouterModelId",
+	"openRouterModelInfo",
+	"openRouterBaseUrl",
+	"openRouterUseMiddleOutTransform",
+	"allowedCommands",
+	"soundEnabled",
+	"soundVolume",
+	"diffEnabled",
+	"enableCheckpoints",
+	"checkpointStorage",
+	"browserViewportSize",
+	"screenshotQuality",
+	"fuzzyMatchThreshold",
+	"preferredLanguage", // Language setting for Cline's communication
+	"writeDelayMs",
+	"terminalOutputLineLimit",
+	"mcpEnabled",
+	"enableMcpServerCreation",
+	"alwaysApproveResubmit",
+	"requestDelaySeconds",
+	"rateLimitSeconds",
+	"currentApiConfigName",
+	"listApiConfigMeta",
+	"vsCodeLmModelSelector",
+	"mode",
+	"modeApiConfigs",
+	"customModePrompts",
+	"customSupportPrompts",
+	"enhancementApiConfigId",
+	"experiments", // Map of experiment IDs to their enabled state
+	"autoApprovalEnabled",
+	"customModes", // Array of custom modes
+	"unboundModelId",
+	"requestyModelId",
+	"requestyModelInfo",
+	"unboundModelInfo",
+	"modelTemperature",
+	"modelMaxTokens",
+	"mistralCodestralUrl",
+	"maxOpenTabsContext",
+	"browserToolEnabled",
+	"lmStudioSpeculativeDecodingEnabled",
+	"lmStudioDraftModelId",
+] as const
+
+// Derive the type from the array - creates a union of string literals
+export type GlobalStateKey = (typeof GLOBAL_STATE_KEYS)[number]

+ 0 - 1
webview-ui/src/components/common/MermaidBlock.tsx

@@ -150,7 +150,6 @@ export default function MermaidBlock({ code }: MermaidBlockProps) {
 }
 
 async function svgToPng(svgEl: SVGElement): Promise<string> {
-	console.log("svgToPng function called")
 	// Clone the SVG to avoid modifying the original
 	const svgClone = svgEl.cloneNode(true) as SVGElement
 

+ 51 - 88
webview-ui/src/components/history/HistoryPreview.tsx

@@ -14,101 +14,64 @@ type HistoryPreviewProps = {
 const HistoryPreview = ({ showHistoryView }: HistoryPreviewProps) => {
 	const { taskHistory } = useExtensionState()
 
-	const handleHistorySelect = (id: string) => {
-		vscode.postMessage({ type: "showTaskWithId", text: id })
-	}
-
 	return (
-		<div style={{ flexShrink: 0 }}>
-			<style>
-				{`
-					.history-preview-item {
-						background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 65%, transparent);
-						border-radius: 4px;
-						position: relative;
-						overflow: hidden;
-						opacity: 0.8;
-						cursor: pointer;
-						margin-bottom: 12px;
-					}
-					.history-preview-item:hover {
-						background-color: color-mix(in srgb, var(--vscode-toolbar-hoverBackground) 100%, transparent);
-						opacity: 1;
-						pointer-events: auto;
-					}
-				`}
-			</style>
-			<div
-				style={{
-					color: "var(--vscode-descriptionForeground)",
-					margin: "10px 20px 10px 20px",
-					display: "flex",
-					alignItems: "center",
-				}}>
-				<span className="codicon codicon-comment-discussion scale-90 mr-1" />
-				<span className="font-medium text-xs uppercase">Recent Tasks</span>
+		<div className="flex flex-col gap-3 shrink-0 mx-5">
+			<div className="flex items-center justify-between text-vscode-descriptionForeground">
+				<div className="flex items-center gap-1">
+					<span className="codicon codicon-comment-discussion scale-90 mr-1" />
+					<span className="font-medium text-xs uppercase">Recent Tasks</span>
+				</div>
+				<Button variant="ghost" size="sm" onClick={() => showHistoryView()} className="uppercase">
+					View All
+				</Button>
 			</div>
-			<div className="px-5">
-				{taskHistory
-					.filter((item) => item.ts && item.task)
-					.slice(0, 3)
-					.map((item) => (
+			{taskHistory.slice(0, 3).map((item) => (
+				<div
+					key={item.id}
+					className="bg-vscode-toolbar-hoverBackground/50 hover:bg-vscode-toolbar-hoverBackground/75 rounded-xs relative overflow-hidden opacity-90 hover:opacity-100 cursor-pointer"
+					onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
+					<div className="flex flex-col gap-2 p-3 pt-1">
+						<div className="flex justify-between items-center">
+							<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
+								{formatDate(item.ts)}
+							</span>
+							<CopyButton itemTask={item.task} />
+						</div>
 						<div
-							key={item.id}
-							className="history-preview-item"
-							onClick={() => handleHistorySelect(item.id)}>
-							<div className="flex flex-col gap-2 p-3 pt-1">
-								<div className="flex justify-between items-center">
-									<span className="text-xs font-medium text-vscode-descriptionForeground uppercase">
-										{formatDate(item.ts)}
-									</span>
-									<CopyButton itemTask={item.task} />
-								</div>
-								<div
-									className="text-vscode-descriptionForeground overflow-hidden whitespace-pre-wrap"
-									style={{
-										display: "-webkit-box",
-										WebkitLineClamp: 3,
-										WebkitBoxOrient: "vertical",
-										wordBreak: "break-word",
-										overflowWrap: "anywhere",
-									}}>
-									{item.task}
-								</div>
-								<div className="text-xs text-vscode-descriptionForeground">
+							className="text-vscode-descriptionForeground overflow-hidden whitespace-pre-wrap"
+							style={{
+								display: "-webkit-box",
+								WebkitLineClamp: 3,
+								WebkitBoxOrient: "vertical",
+								wordBreak: "break-word",
+								overflowWrap: "anywhere",
+							}}>
+							{item.task}
+						</div>
+						<div className="text-xs text-vscode-descriptionForeground">
+							<span>
+								Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
+								{formatLargeNumber(item.tokensOut || 0)}
+							</span>
+							{!!item.cacheWrites && (
+								<>
+									{" • "}
 									<span>
-										Tokens: ↑{formatLargeNumber(item.tokensIn || 0)} ↓
-										{formatLargeNumber(item.tokensOut || 0)}
+										Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
+										{formatLargeNumber(item.cacheReads || 0)}
 									</span>
-									{!!item.cacheWrites && (
-										<>
-											{" • "}
-											<span>
-												Cache: +{formatLargeNumber(item.cacheWrites || 0)} →{" "}
-												{formatLargeNumber(item.cacheReads || 0)}
-											</span>
-										</>
-									)}
-									{!!item.totalCost && (
-										<>
-											{" • "}
-											<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
-										</>
-									)}
-								</div>
-							</div>
+								</>
+							)}
+							{!!item.totalCost && (
+								<>
+									{" • "}
+									<span>API Cost: ${item.totalCost?.toFixed(4)}</span>
+								</>
+							)}
 						</div>
-					))}
-				<div className="flex justify-center">
-					<Button
-						variant="ghost"
-						size="sm"
-						onClick={() => showHistoryView()}
-						className="font-normal text-vscode-descriptionForeground">
-						View all history
-					</Button>
+					</div>
 				</div>
-			</div>
+			))}
 		</div>
 	)
 }

+ 17 - 98
webview-ui/src/components/history/HistoryView.tsx

@@ -1,16 +1,15 @@
-import React, { memo, useMemo, useState, useEffect } from "react"
+import React, { memo, useState } from "react"
 import { DeleteTaskDialog } from "./DeleteTaskDialog"
-import { Fzf } from "fzf"
 import prettyBytes from "pretty-bytes"
 import { Virtuoso } from "react-virtuoso"
 import { VSCodeButton, VSCodeTextField, VSCodeRadioGroup, VSCodeRadio } from "@vscode/webview-ui-toolkit/react"
 
 import { vscode } from "@/utils/vscode"
 import { formatLargeNumber, formatDate } from "@/utils/format"
-import { highlightFzfMatch } from "@/utils/highlight"
+import { cn } from "@/lib/utils"
 import { Button } from "@/components/ui"
 
-import { useExtensionState } from "../../context/ExtensionStateContext"
+import { useTaskSearch } from "./useTaskSearch"
 import { ExportButton } from "./ExportButton"
 import { CopyButton } from "./CopyButton"
 
@@ -21,95 +20,18 @@ type HistoryViewProps = {
 type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
 
 const HistoryView = ({ onDone }: HistoryViewProps) => {
-	const { taskHistory } = useExtensionState()
-	const [searchQuery, setSearchQuery] = useState("")
-	const [sortOption, setSortOption] = useState<SortOption>("newest")
-	const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
-
-	useEffect(() => {
-		if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
-			setLastNonRelevantSort(sortOption)
-			setSortOption("mostRelevant")
-		} else if (!searchQuery && sortOption === "mostRelevant" && lastNonRelevantSort) {
-			setSortOption(lastNonRelevantSort)
-			setLastNonRelevantSort(null)
-		}
-	}, [searchQuery, sortOption, lastNonRelevantSort])
-
-	const handleHistorySelect = (id: string) => {
-		vscode.postMessage({ type: "showTaskWithId", text: id })
-	}
+	const { tasks, searchQuery, setSearchQuery, sortOption, setSortOption, setLastNonRelevantSort } = useTaskSearch()
 
 	const [deleteTaskId, setDeleteTaskId] = useState<string | null>(null)
 
-	const presentableTasks = useMemo(() => {
-		return taskHistory.filter((item) => item.ts && item.task)
-	}, [taskHistory])
-
-	const fzf = useMemo(() => {
-		return new Fzf(presentableTasks, {
-			selector: (item) => item.task,
-		})
-	}, [presentableTasks])
-
-	const taskHistorySearchResults = useMemo(() => {
-		let results = presentableTasks
-		if (searchQuery) {
-			const searchResults = fzf.find(searchQuery)
-			results = searchResults.map((result) => ({
-				...result.item,
-				task: highlightFzfMatch(result.item.task, Array.from(result.positions)),
-			}))
-		}
-
-		// First apply search if needed
-		const searchResults = searchQuery ? results : presentableTasks
-
-		// Then sort the results
-		return [...searchResults].sort((a, b) => {
-			switch (sortOption) {
-				case "oldest":
-					return (a.ts || 0) - (b.ts || 0)
-				case "mostExpensive":
-					return (b.totalCost || 0) - (a.totalCost || 0)
-				case "mostTokens":
-					const aTokens = (a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0)
-					const bTokens = (b.tokensIn || 0) + (b.tokensOut || 0) + (b.cacheWrites || 0) + (b.cacheReads || 0)
-					return bTokens - aTokens
-				case "mostRelevant":
-					// Keep fuse order if searching, otherwise sort by newest
-					return searchQuery ? 0 : (b.ts || 0) - (a.ts || 0)
-				case "newest":
-				default:
-					return (b.ts || 0) - (a.ts || 0)
-			}
-		})
-	}, [presentableTasks, searchQuery, fzf, sortOption])
-
 	return (
-		<div
-			style={{
-				position: "fixed",
-				top: 0,
-				left: 0,
-				right: 0,
-				bottom: 0,
-				display: "flex",
-				flexDirection: "column",
-				overflow: "hidden",
-			}}>
-			<div
-				style={{
-					display: "flex",
-					justifyContent: "space-between",
-					alignItems: "center",
-					padding: "10px 17px 10px 20px",
-				}}>
-				<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>History</h3>
-				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
-			</div>
-			<div style={{ padding: "5px 17px 6px 17px" }}>
-				<div style={{ display: "flex", flexDirection: "column", gap: "6px" }}>
+		<div className="fixed inset-0 flex flex-col">
+			<div className="flex flex-col gap-2 px-5 py-2.5 border-b border-vscode-panel-border">
+				<div className="flex justify-between items-center">
+					<h3 className="text-vscode-foreground m-0">History</h3>
+					<VSCodeButton onClick={onDone}>Done</VSCodeButton>
+				</div>
+				<div className="flex flex-col gap-2">
 					<VSCodeTextField
 						style={{ width: "100%" }}
 						placeholder="Fuzzy search history..."
@@ -166,7 +88,7 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 						flexGrow: 1,
 						overflowY: "scroll",
 					}}
-					data={taskHistorySearchResults}
+					data={tasks}
 					data-testid="virtuoso-container"
 					components={{
 						List: React.forwardRef((props, ref) => (
@@ -175,15 +97,12 @@ const HistoryView = ({ onDone }: HistoryViewProps) => {
 					}}
 					itemContent={(index, item) => (
 						<div
-							key={item.id}
 							data-testid={`task-item-${item.id}`}
-							className="history-item"
-							style={{
-								cursor: "pointer",
-								borderBottom:
-									index < taskHistory.length - 1 ? "1px solid var(--vscode-panel-border)" : "none",
-							}}
-							onClick={() => handleHistorySelect(item.id)}>
+							key={item.id}
+							className={cn("cursor-pointer", {
+								"border-b border-vscode-panel-border": index < tasks.length - 1,
+							})}
+							onClick={() => vscode.postMessage({ type: "showTaskWithId", text: item.id })}>
 							<div
 								style={{
 									display: "flex",

+ 78 - 0
webview-ui/src/components/history/useTaskSearch.ts

@@ -0,0 +1,78 @@
+import { useState, useEffect, useMemo } from "react"
+import { Fzf } from "fzf"
+
+import { highlightFzfMatch } from "@/utils/highlight"
+import { useExtensionState } from "@/context/ExtensionStateContext"
+
+type SortOption = "newest" | "oldest" | "mostExpensive" | "mostTokens" | "mostRelevant"
+
+export const useTaskSearch = () => {
+	const { taskHistory } = useExtensionState()
+	const [searchQuery, setSearchQuery] = useState("")
+	const [sortOption, setSortOption] = useState<SortOption>("newest")
+	const [lastNonRelevantSort, setLastNonRelevantSort] = useState<SortOption | null>("newest")
+
+	useEffect(() => {
+		if (searchQuery && sortOption !== "mostRelevant" && !lastNonRelevantSort) {
+			setLastNonRelevantSort(sortOption)
+			setSortOption("mostRelevant")
+		} else if (!searchQuery && sortOption === "mostRelevant" && lastNonRelevantSort) {
+			setSortOption(lastNonRelevantSort)
+			setLastNonRelevantSort(null)
+		}
+	}, [searchQuery, sortOption, lastNonRelevantSort])
+
+	const presentableTasks = useMemo(() => {
+		return taskHistory.filter((item) => item.ts && item.task)
+	}, [taskHistory])
+
+	const fzf = useMemo(() => {
+		return new Fzf(presentableTasks, {
+			selector: (item) => item.task,
+		})
+	}, [presentableTasks])
+
+	const tasks = useMemo(() => {
+		let results = presentableTasks
+		if (searchQuery) {
+			const searchResults = fzf.find(searchQuery)
+			results = searchResults.map((result) => ({
+				...result.item,
+				task: highlightFzfMatch(result.item.task, Array.from(result.positions)),
+			}))
+		}
+
+		// First apply search if needed
+		const searchResults = searchQuery ? results : presentableTasks
+
+		// Then sort the results
+		return [...searchResults].sort((a, b) => {
+			switch (sortOption) {
+				case "oldest":
+					return (a.ts || 0) - (b.ts || 0)
+				case "mostExpensive":
+					return (b.totalCost || 0) - (a.totalCost || 0)
+				case "mostTokens":
+					const aTokens = (a.tokensIn || 0) + (a.tokensOut || 0) + (a.cacheWrites || 0) + (a.cacheReads || 0)
+					const bTokens = (b.tokensIn || 0) + (b.tokensOut || 0) + (b.cacheWrites || 0) + (b.cacheReads || 0)
+					return bTokens - aTokens
+				case "mostRelevant":
+					// Keep fuse order if searching, otherwise sort by newest
+					return searchQuery ? 0 : (b.ts || 0) - (a.ts || 0)
+				case "newest":
+				default:
+					return (b.ts || 0) - (a.ts || 0)
+			}
+		})
+	}, [presentableTasks, searchQuery, fzf, sortOption])
+
+	return {
+		tasks,
+		searchQuery,
+		setSearchQuery,
+		sortOption,
+		setSortOption,
+		lastNonRelevantSort,
+		setLastNonRelevantSort,
+	}
+}

+ 4 - 23
webview-ui/src/components/mcp/McpView.tsx

@@ -29,28 +29,12 @@ const McpView = ({ onDone }: McpViewProps) => {
 	} = useExtensionState()
 
 	return (
-		<div
-			style={{
-				position: "fixed",
-				top: 0,
-				left: 0,
-				right: 0,
-				bottom: 0,
-				display: "flex",
-				flexDirection: "column",
-			}}>
-			<div
-				style={{
-					display: "flex",
-					justifyContent: "space-between",
-					alignItems: "center",
-					padding: "10px 17px 10px 20px",
-				}}>
-				<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>MCP Servers</h3>
+		<div className="fixed inset-0 flex flex-col">
+			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
+				<h3 className="text-vscode-foreground m-0">MCP Servers</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 			</div>
-
-			<div style={{ flex: 1, overflow: "auto", padding: "0 20px" }}>
+			<div className="flex-1 overflow-auto p-5">
 				<div
 					style={{
 						color: "var(--vscode-foreground)",
@@ -119,9 +103,6 @@ const McpView = ({ onDone }: McpViewProps) => {
 						</div>
 					</>
 				)}
-
-				{/* Bottom padding */}
-				<div style={{ height: "20px" }} />
 			</div>
 		</div>
 	)

+ 2 - 4
webview-ui/src/components/prompts/PromptsView.tsx

@@ -407,12 +407,11 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 
 	return (
 		<div className="fixed inset-0 flex flex-col">
-			<div className="flex justify-between items-center px-5 py-2.5">
+			<div className="flex justify-between items-center px-5 py-2.5 border-b border-vscode-panel-border">
 				<h3 className="text-vscode-foreground m-0">Prompts</h3>
 				<VSCodeButton onClick={onDone}>Done</VSCodeButton>
 			</div>
-
-			<div className="flex-1 overflow-auto px-5">
+			<div className="flex-1 overflow-auto p-5">
 				<div className="pb-5 border-b border-vscode-input-border">
 					<div className="mb-5">
 						<div className="font-bold mb-1">Preferred Language</div>
@@ -1174,7 +1173,6 @@ const PromptsView = ({ onDone }: PromptsViewProps) => {
 					</div>
 				</div>
 			</div>
-
 			{isCreateModeDialogOpen && (
 				<div
 					style={{

+ 176 - 0
webview-ui/src/components/settings/AdvancedSettings.tsx

@@ -0,0 +1,176 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
+import { Cog } from "lucide-react"
+
+import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
+
+import { cn } from "@/lib/utils"
+
+import { SetCachedStateField, SetExperimentEnabled } from "./types"
+import { sliderLabelStyle } from "./styles"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+import { ExperimentalFeature } from "./ExperimentalFeature"
+
+type AdvancedSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	rateLimitSeconds: number
+	terminalOutputLineLimit?: number
+	maxOpenTabsContext: number
+	diffEnabled?: boolean
+	fuzzyMatchThreshold?: number
+	setCachedStateField: SetCachedStateField<
+		"rateLimitSeconds" | "terminalOutputLineLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold"
+	>
+	experiments: Record<ExperimentId, boolean>
+	setExperimentEnabled: SetExperimentEnabled
+}
+
+export const AdvancedSettings = ({
+	rateLimitSeconds,
+	terminalOutputLineLimit,
+	maxOpenTabsContext,
+	diffEnabled,
+	fuzzyMatchThreshold,
+	setCachedStateField,
+	experiments,
+	setExperimentEnabled,
+	className,
+	...props
+}: AdvancedSettingsProps) => {
+	return (
+		<div className={cn("flex flex-col gap-2", className)} {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<Cog className="w-4" />
+					<div>Advanced</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<div className="flex flex-col gap-2">
+						<span className="font-medium">Rate limit</span>
+						<div className="flex items-center gap-2">
+							<input
+								type="range"
+								min="0"
+								max="60"
+								step="1"
+								value={rateLimitSeconds}
+								onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
+								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+							/>
+							<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
+						</div>
+					</div>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">Minimum time between API requests.</p>
+				</div>
+
+				<div>
+					<div className="flex flex-col gap-2">
+						<span className="font-medium">Terminal output limit</span>
+						<div className="flex items-center gap-2">
+							<input
+								type="range"
+								min="100"
+								max="5000"
+								step="100"
+								value={terminalOutputLineLimit ?? 500}
+								onChange={(e) =>
+									setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
+								}
+								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+							/>
+							<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
+						</div>
+					</div>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						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>
+					<div className="flex flex-col gap-2">
+						<span className="font-medium">Open tabs context limit</span>
+						<div className="flex items-center gap-2">
+							<input
+								type="range"
+								min="0"
+								max="500"
+								step="1"
+								value={maxOpenTabsContext ?? 20}
+								onChange={(e) => setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))}
+								className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+							/>
+							<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
+						</div>
+					</div>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Maximum number of VSCode open tabs to include in context. Higher values provide more context but
+						increase token usage.
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={diffEnabled}
+						onChange={(e: any) => {
+							setCachedStateField("diffEnabled", e.target.checked)
+							if (!e.target.checked) {
+								// Reset experimental strategy when diffs are disabled.
+								setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
+							}
+						}}>
+						<span className="font-medium">Enable editing through diffs</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will be able to edit files more quickly and will automatically reject
+						truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model.
+					</p>
+					{diffEnabled && (
+						<div
+							style={{
+								display: "flex",
+								flexDirection: "column",
+								gap: "5px",
+								marginTop: "10px",
+								marginBottom: "10px",
+								paddingLeft: "10px",
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<span className="font-medium">Match precision</span>
+							<div className="flex items-center gap-2">
+								<input
+									type="range"
+									min="0.8"
+									max="1"
+									step="0.005"
+									value={fuzzyMatchThreshold ?? 1.0}
+									onChange={(e) => {
+										setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value))
+									}}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								/>
+								<span style={{ ...sliderLabelStyle }}>
+									{Math.round((fuzzyMatchThreshold || 1) * 100)}%
+								</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-0">
+								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>
+							<ExperimentalFeature
+								key={EXPERIMENT_IDS.DIFF_STRATEGY}
+								{...experimentConfigsMap.DIFF_STRATEGY}
+								enabled={experiments[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false}
+								onChange={(enabled) => setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)}
+							/>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 170 - 181
webview-ui/src/components/settings/ApiConfigManager.tsx

@@ -142,199 +142,188 @@ const ApiConfigManager = ({
 	const isOnlyProfile = listApiConfigMeta?.length === 1
 
 	return (
-		<div style={{ marginBottom: 5 }}>
-			<div
-				style={{
-					display: "flex",
-					flexDirection: "column",
-					gap: "2px",
-				}}>
-				<label htmlFor="config-profile">
-					<span style={{ fontWeight: "500" }}>Configuration Profile</span>
-				</label>
+		<div className="flex flex-col gap-1">
+			<label htmlFor="config-profile">
+				<span className="font-medium">Configuration Profile</span>
+			</label>
 
-				{isRenaming ? (
-					<div
-						data-testid="rename-form"
-						style={{ display: "flex", gap: "4px", alignItems: "center", flexDirection: "column" }}>
-						<div style={{ display: "flex", gap: "4px", alignItems: "center", width: "100%" }}>
-							<VSCodeTextField
-								ref={inputRef}
-								value={inputValue}
-								onInput={(e: unknown) => {
-									const target = e as { target: { value: string } }
-									setInputValue(target.target.value)
-									setError(null)
-								}}
-								placeholder="Enter new name"
-								style={{ flexGrow: 1 }}
-								onKeyDown={(e: unknown) => {
-									const event = e as { key: string }
-									if (event.key === "Enter" && inputValue.trim()) {
-										handleSave()
-									} else if (event.key === "Escape") {
-										handleCancel()
-									}
-								}}
-							/>
-							<VSCodeButton
-								appearance="icon"
-								disabled={!inputValue.trim()}
-								onClick={handleSave}
-								title="Save"
-								style={{
-									padding: 0,
-									margin: 0,
-									height: "28px",
-									width: "28px",
-									minWidth: "28px",
-								}}>
-								<span className="codicon codicon-check" />
-							</VSCodeButton>
-							<VSCodeButton
-								appearance="icon"
-								onClick={handleCancel}
-								title="Cancel"
-								style={{
-									padding: 0,
-									margin: 0,
-									height: "28px",
-									width: "28px",
-									minWidth: "28px",
-								}}>
-								<span className="codicon codicon-close" />
-							</VSCodeButton>
-						</div>
-						{error && (
-							<p className="text-red-500 text-sm mt-2" data-testid="error-message">
-								{error}
-							</p>
-						)}
-					</div>
-				) : (
-					<>
-						<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
-							<Dropdown
-								id="config-profile"
-								value={currentApiConfigName}
-								onChange={(value: unknown) => {
-									onSelectConfig((value as DropdownOption).value)
-								}}
-								style={{
-									minWidth: 130,
-									zIndex: 1002,
-								}}
-								role="combobox"
-								options={listApiConfigMeta.map((config) => ({
-									value: config.name,
-									label: config.name,
-								}))}
-							/>
-							<VSCodeButton
-								appearance="icon"
-								onClick={handleAdd}
-								title="Add profile"
-								style={{
-									padding: 0,
-									margin: 0,
-									height: "28px",
-									width: "28px",
-									minWidth: "28px",
-								}}>
-								<span className="codicon codicon-add" />
-							</VSCodeButton>
-							{currentApiConfigName && (
-								<>
-									<VSCodeButton
-										appearance="icon"
-										onClick={handleStartRename}
-										title="Rename profile"
-										style={{
-											padding: 0,
-											margin: 0,
-											height: "28px",
-											width: "28px",
-											minWidth: "28px",
-										}}>
-										<span className="codicon codicon-edit" />
-									</VSCodeButton>
-									<VSCodeButton
-										appearance="icon"
-										onClick={handleDelete}
-										title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
-										disabled={isOnlyProfile}
-										style={{
-											padding: 0,
-											margin: 0,
-											height: "28px",
-											width: "28px",
-											minWidth: "28px",
-										}}>
-										<span className="codicon codicon-trash" />
-									</VSCodeButton>
-								</>
-							)}
-						</div>
-						<p
-							style={{
-								fontSize: "12px",
-								margin: "5px 0 12px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							Save different API configurations to quickly switch between providers and settings
-						</p>
-					</>
-				)}
-
-				<Dialog
-					open={isCreating}
-					onOpenChange={(open: boolean) => {
-						if (open) {
-							setIsCreating(true)
-							setNewProfileName("")
-							setError(null)
-						} else {
-							resetCreateState()
-						}
-					}}
-					aria-labelledby="new-profile-title">
-					<DialogContent className="p-4 max-w-sm">
-						<DialogTitle>New Configuration Profile</DialogTitle>
-						<Input
-							ref={newProfileInputRef}
-							value={newProfileName}
+			{isRenaming ? (
+				<div
+					data-testid="rename-form"
+					style={{ display: "flex", gap: "4px", alignItems: "center", flexDirection: "column" }}>
+					<div style={{ display: "flex", gap: "4px", alignItems: "center", width: "100%" }}>
+						<VSCodeTextField
+							ref={inputRef}
+							value={inputValue}
 							onInput={(e: unknown) => {
 								const target = e as { target: { value: string } }
-								setNewProfileName(target.target.value)
+								setInputValue(target.target.value)
 								setError(null)
 							}}
-							placeholder="Enter profile name"
-							style={{ width: "100%" }}
+							placeholder="Enter new name"
+							style={{ flexGrow: 1 }}
 							onKeyDown={(e: unknown) => {
 								const event = e as { key: string }
-								if (event.key === "Enter" && newProfileName.trim()) {
-									handleNewProfileSave()
+								if (event.key === "Enter" && inputValue.trim()) {
+									handleSave()
 								} else if (event.key === "Escape") {
-									resetCreateState()
+									handleCancel()
 								}
 							}}
 						/>
-						{error && (
-							<p className="text-red-500 text-sm mt-2" data-testid="error-message">
-								{error}
-							</p>
+						<VSCodeButton
+							appearance="icon"
+							disabled={!inputValue.trim()}
+							onClick={handleSave}
+							title="Save"
+							style={{
+								padding: 0,
+								margin: 0,
+								height: "28px",
+								width: "28px",
+								minWidth: "28px",
+							}}>
+							<span className="codicon codicon-check" />
+						</VSCodeButton>
+						<VSCodeButton
+							appearance="icon"
+							onClick={handleCancel}
+							title="Cancel"
+							style={{
+								padding: 0,
+								margin: 0,
+								height: "28px",
+								width: "28px",
+								minWidth: "28px",
+							}}>
+							<span className="codicon codicon-close" />
+						</VSCodeButton>
+					</div>
+					{error && (
+						<p className="text-red-500 text-sm mt-2" data-testid="error-message">
+							{error}
+						</p>
+					)}
+				</div>
+			) : (
+				<>
+					<div style={{ display: "flex", gap: "4px", alignItems: "center" }}>
+						<Dropdown
+							id="config-profile"
+							value={currentApiConfigName}
+							onChange={(value: unknown) => {
+								onSelectConfig((value as DropdownOption).value)
+							}}
+							role="combobox"
+							options={listApiConfigMeta.map((config) => ({
+								value: config.name,
+								label: config.name,
+							}))}
+						/>
+						<VSCodeButton
+							appearance="icon"
+							onClick={handleAdd}
+							title="Add profile"
+							style={{
+								padding: 0,
+								margin: 0,
+								height: "28px",
+								width: "28px",
+								minWidth: "28px",
+							}}>
+							<span className="codicon codicon-add" />
+						</VSCodeButton>
+						{currentApiConfigName && (
+							<>
+								<VSCodeButton
+									appearance="icon"
+									onClick={handleStartRename}
+									title="Rename profile"
+									style={{
+										padding: 0,
+										margin: 0,
+										height: "28px",
+										width: "28px",
+										minWidth: "28px",
+									}}>
+									<span className="codicon codicon-edit" />
+								</VSCodeButton>
+								<VSCodeButton
+									appearance="icon"
+									onClick={handleDelete}
+									title={isOnlyProfile ? "Cannot delete the only profile" : "Delete profile"}
+									disabled={isOnlyProfile}
+									style={{
+										padding: 0,
+										margin: 0,
+										height: "28px",
+										width: "28px",
+										minWidth: "28px",
+									}}>
+									<span className="codicon codicon-trash" />
+								</VSCodeButton>
+							</>
 						)}
-						<div className="flex justify-end gap-2 mt-4">
-							<Button variant="secondary" onClick={resetCreateState}>
-								Cancel
-							</Button>
-							<Button variant="default" disabled={!newProfileName.trim()} onClick={handleNewProfileSave}>
-								Create Profile
-							</Button>
-						</div>
-					</DialogContent>
-				</Dialog>
-			</div>
+					</div>
+					<p
+						style={{
+							fontSize: "12px",
+							margin: "5px 0 12px",
+							color: "var(--vscode-descriptionForeground)",
+						}}>
+						Save different API configurations to quickly switch between providers and settings.
+					</p>
+				</>
+			)}
+
+			<Dialog
+				open={isCreating}
+				onOpenChange={(open: boolean) => {
+					if (open) {
+						setIsCreating(true)
+						setNewProfileName("")
+						setError(null)
+					} else {
+						resetCreateState()
+					}
+				}}
+				aria-labelledby="new-profile-title">
+				<DialogContent className="p-4 max-w-sm">
+					<DialogTitle>New Configuration Profile</DialogTitle>
+					<Input
+						ref={newProfileInputRef}
+						value={newProfileName}
+						onInput={(e: unknown) => {
+							const target = e as { target: { value: string } }
+							setNewProfileName(target.target.value)
+							setError(null)
+						}}
+						placeholder="Enter profile name"
+						style={{ width: "100%" }}
+						onKeyDown={(e: unknown) => {
+							const event = e as { key: string }
+							if (event.key === "Enter" && newProfileName.trim()) {
+								handleNewProfileSave()
+							} else if (event.key === "Escape") {
+								resetCreateState()
+							}
+						}}
+					/>
+					{error && (
+						<p className="text-red-500 text-sm mt-2" data-testid="error-message">
+							{error}
+						</p>
+					)}
+					<div className="flex justify-end gap-2 mt-4">
+						<Button variant="secondary" onClick={resetCreateState}>
+							Cancel
+						</Button>
+						<Button variant="default" disabled={!newProfileName.trim()} onClick={handleNewProfileSave}>
+							Create Profile
+						</Button>
+					</div>
+				</DialogContent>
+			</Dialog>
 		</div>
 	)
 }

+ 73 - 2
webview-ui/src/components/settings/ApiOptions.tsx

@@ -37,7 +37,6 @@ import { ExtensionMessage } from "../../../../src/shared/ExtensionMessage"
 import { vscode } from "../../utils/vscode"
 import VSCodeButtonLink from "../common/VSCodeButtonLink"
 import { ModelInfoView } from "./ModelInfoView"
-import { DROPDOWN_Z_INDEX } from "./styles"
 import { ModelPicker } from "./ModelPicker"
 import { TemperatureControl } from "./TemperatureControl"
 import { validateApiConfiguration, validateModelId } from "@/utils/validate"
@@ -242,7 +241,6 @@ const ApiOptions = ({
 					id="api-provider"
 					value={selectedProvider}
 					onChange={handleInputChange("apiProvider", dropdownEventTransform)}
-					style={{ minWidth: 130, position: "relative", zIndex: DROPDOWN_Z_INDEX + 1 }}
 					options={[
 						{ value: "openrouter", label: "OpenRouter" },
 						{ value: "anthropic", label: "Anthropic" },
@@ -1108,6 +1106,79 @@ const ApiOptions = ({
 							))}
 						</VSCodeRadioGroup>
 					)}
+					<div style={{ display: "flex", alignItems: "center", marginTop: "16px", marginBottom: "8px" }}>
+						<Checkbox
+							checked={apiConfiguration?.lmStudioSpeculativeDecodingEnabled === true}
+							onChange={(checked) => {
+								// Explicitly set the boolean value using direct method
+								setApiConfigurationField("lmStudioSpeculativeDecodingEnabled", checked)
+							}}>
+							Enable Speculative Decoding
+						</Checkbox>
+					</div>
+					{apiConfiguration?.lmStudioSpeculativeDecodingEnabled && (
+						<>
+							<VSCodeTextField
+								value={apiConfiguration?.lmStudioDraftModelId || ""}
+								style={{ width: "100%" }}
+								onInput={handleInputChange("lmStudioDraftModelId")}
+								placeholder={"e.g. lmstudio-community/llama-3.2-1b-instruct"}>
+								<span className="font-medium">Draft Model ID</span>
+							</VSCodeTextField>
+							<div
+								style={{
+									fontSize: "11px",
+									color: "var(--vscode-descriptionForeground)",
+									marginTop: 4,
+									display: "flex",
+									alignItems: "center",
+									gap: 4,
+								}}>
+								<i className="codicon codicon-info" style={{ fontSize: "12px" }}></i>
+								<span>
+									Draft model must be from the same model family for speculative decoding to work
+									correctly.
+								</span>
+							</div>
+							{lmStudioModels.length > 0 && (
+								<>
+									<div style={{ marginTop: "8px" }}>
+										<span className="font-medium">Select Draft Model</span>
+									</div>
+									<VSCodeRadioGroup
+										value={
+											lmStudioModels.includes(apiConfiguration?.lmStudioDraftModelId || "")
+												? apiConfiguration?.lmStudioDraftModelId
+												: ""
+										}
+										onChange={handleInputChange("lmStudioDraftModelId")}>
+										{lmStudioModels.map((model) => (
+											<VSCodeRadio key={`draft-${model}`} value={model}>
+												{model}
+											</VSCodeRadio>
+										))}
+									</VSCodeRadioGroup>
+									{lmStudioModels.length === 0 && (
+										<div
+											style={{
+												fontSize: "12px",
+												marginTop: "8px",
+												padding: "6px",
+												backgroundColor: "var(--vscode-inputValidation-infoBackground)",
+												border: "1px solid var(--vscode-inputValidation-infoBorder)",
+												borderRadius: "3px",
+												color: "var(--vscode-inputValidation-infoForeground)",
+											}}>
+											<i className="codicon codicon-info" style={{ marginRight: "5px" }}></i>
+											No draft models found. Please ensure LM Studio is running with Server Mode
+											enabled.
+										</div>
+									)}
+								</>
+							)}
+						</>
+					)}
+
 					<p
 						style={{
 							fontSize: "12px",

+ 252 - 0
webview-ui/src/components/settings/AutoApproveSettings.tsx

@@ -0,0 +1,252 @@
+import { HTMLAttributes, useState } from "react"
+import { VSCodeButton, VSCodeCheckbox, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
+import { CheckCheck } from "lucide-react"
+
+import { vscode } from "@/utils/vscode"
+import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
+
+import { SetCachedStateField } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type AutoApproveSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	alwaysAllowReadOnly?: boolean
+	alwaysAllowWrite?: boolean
+	writeDelayMs: number
+	alwaysAllowBrowser?: boolean
+	alwaysApproveResubmit?: boolean
+	requestDelaySeconds: number
+	alwaysAllowMcp?: boolean
+	alwaysAllowModeSwitch?: boolean
+	alwaysAllowExecute?: boolean
+	allowedCommands?: string[]
+	setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType>
+}
+
+export const AutoApproveSettings = ({
+	alwaysAllowReadOnly,
+	alwaysAllowWrite,
+	writeDelayMs,
+	alwaysAllowBrowser,
+	alwaysApproveResubmit,
+	requestDelaySeconds,
+	alwaysAllowMcp,
+	alwaysAllowModeSwitch,
+	alwaysAllowExecute,
+	allowedCommands,
+	setCachedStateField,
+	className,
+	...props
+}: AutoApproveSettingsProps) => {
+	const [commandInput, setCommandInput] = useState("")
+
+	const handleAddCommand = () => {
+		const currentCommands = allowedCommands ?? []
+		if (commandInput && !currentCommands.includes(commandInput)) {
+			const newCommands = [...currentCommands, commandInput]
+			setCachedStateField("allowedCommands", newCommands)
+			setCommandInput("")
+			vscode.postMessage({ type: "allowedCommands", commands: newCommands })
+		}
+	}
+
+	return (
+		<div {...props}>
+			<SectionHeader description="Allow Roo to automatically perform operations without requiring approval. Enable these settings only if you fully trust the AI and understand the associated security risks.">
+				<div className="flex items-center gap-2">
+					<CheckCheck className="w-4" />
+					<div>Auto-Approve</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowReadOnly}
+						onChange={(e: any) => setCachedStateField("alwaysAllowReadOnly", e.target.checked)}>
+						<span className="font-medium">Always approve read-only operations</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will automatically view directory contents and read files without requiring
+						you to click the Approve button.
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowWrite}
+						onChange={(e: any) => setCachedStateField("alwaysAllowWrite", e.target.checked)}>
+						<span className="font-medium">Always approve write operations</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically create and edit files without requiring approval
+					</p>
+					{alwaysAllowWrite && (
+						<div
+							style={{
+								marginTop: 10,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
+								<input
+									type="range"
+									min="0"
+									max="5000"
+									step="100"
+									value={writeDelayMs}
+									onChange={(e) => setCachedStateField("writeDelayMs", parseInt(e.target.value))}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								/>
+								<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-1">
+								Delay after writes to allow diagnostics to detect potential problems
+							</p>
+						</div>
+					)}
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowBrowser}
+						onChange={(e: any) => setCachedStateField("alwaysAllowBrowser", e.target.checked)}>
+						<span className="font-medium">Always approve browser actions</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically perform browser actions without requiring approval
+						<br />
+						Note: Only applies when the model supports computer use
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysApproveResubmit}
+						onChange={(e: any) => setCachedStateField("alwaysApproveResubmit", e.target.checked)}>
+						<span className="font-medium">Always retry failed API requests</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically retry failed API requests when server returns an error response
+					</p>
+					{alwaysApproveResubmit && (
+						<div
+							style={{
+								marginTop: 10,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
+								<input
+									type="range"
+									min="5"
+									max="100"
+									step="1"
+									value={requestDelaySeconds}
+									onChange={(e) =>
+										setCachedStateField("requestDelaySeconds", parseInt(e.target.value))
+									}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+								/>
+								<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-0">
+								Delay before retrying the request
+							</p>
+						</div>
+					)}
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowMcp}
+						onChange={(e: any) => setCachedStateField("alwaysAllowMcp", e.target.checked)}>
+						<span className="font-medium">Always approve MCP tools</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this setting
+						and the tool's individual "Always allow" checkbox)
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowModeSwitch}
+						onChange={(e: any) => setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}>
+						<span className="font-medium">Always approve mode switching & task creation</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically switch between different AI modes and create new tasks without requiring approval
+					</p>
+				</div>
+
+				<div>
+					<VSCodeCheckbox
+						checked={alwaysAllowExecute}
+						onChange={(e: any) => setCachedStateField("alwaysAllowExecute", e.target.checked)}>
+						<span className="font-medium">Always approve allowed execute operations</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						Automatically execute allowed terminal commands without requiring approval
+					</p>
+					{alwaysAllowExecute && (
+						<div
+							style={{
+								marginTop: 10,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<span className="font-medium">Allowed Auto-Execute Commands</span>
+							<p className="text-vscode-descriptionForeground text-sm mt-0">
+								Command prefixes that can be auto-executed when "Always approve execute operations" is
+								enabled. Add * to allow all commands (use with caution).
+							</p>
+							<div style={{ display: "flex", gap: "5px", marginTop: "10px" }}>
+								<VSCodeTextField
+									value={commandInput}
+									onInput={(e: any) => setCommandInput(e.target.value)}
+									onKeyDown={(e: any) => {
+										if (e.key === "Enter") {
+											e.preventDefault()
+											handleAddCommand()
+										}
+									}}
+									placeholder="Enter command prefix (e.g., 'git ')"
+									style={{ flexGrow: 1 }}
+								/>
+								<VSCodeButton onClick={handleAddCommand}>Add</VSCodeButton>
+							</div>
+							<div
+								style={{
+									marginTop: "10px",
+									display: "flex",
+									flexWrap: "wrap",
+									gap: "5px",
+								}}>
+								{(allowedCommands ?? []).map((cmd, index) => (
+									<div
+										key={index}
+										className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
+										<span>{cmd}</span>
+										<VSCodeButton
+											appearance="icon"
+											className="text-primary-foreground"
+											onClick={() => {
+												const newCommands = (allowedCommands ?? []).filter(
+													(_, i) => i !== index,
+												)
+												setCachedStateField("allowedCommands", newCommands)
+												vscode.postMessage({ type: "allowedCommands", commands: newCommands })
+											}}>
+											<span className="codicon codicon-close" />
+										</VSCodeButton>
+									</div>
+								))}
+							</div>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 105 - 0
webview-ui/src/components/settings/BrowserSettings.tsx

@@ -0,0 +1,105 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
+import { Dropdown, type DropdownOption } from "vscrui"
+import { SquareMousePointer } from "lucide-react"
+
+import { SetCachedStateField } from "./types"
+import { sliderLabelStyle } from "./styles"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type BrowserSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	browserToolEnabled?: boolean
+	browserViewportSize?: string
+	screenshotQuality?: number
+	setCachedStateField: SetCachedStateField<"browserToolEnabled" | "browserViewportSize" | "screenshotQuality">
+}
+
+export const BrowserSettings = ({
+	browserToolEnabled,
+	browserViewportSize,
+	screenshotQuality,
+	setCachedStateField,
+	...props
+}: BrowserSettingsProps) => {
+	return (
+		<div {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<SquareMousePointer className="w-4" />
+					<div>Browser / Computer Use</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={browserToolEnabled}
+						onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
+						<span className="font-medium">Enable browser tool</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo can use a browser to interact with websites when using models that support
+						computer use.
+					</p>
+					{browserToolEnabled && (
+						<div
+							style={{
+								marginLeft: 0,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div>
+								<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
+									Viewport size
+								</label>
+								<div className="dropdown-container">
+									<Dropdown
+										value={browserViewportSize}
+										onChange={(value: unknown) => {
+											setCachedStateField("browserViewportSize", (value as DropdownOption).value)
+										}}
+										style={{ width: "100%" }}
+										options={[
+											{ value: "1280x800", label: "Large Desktop (1280x800)" },
+											{ value: "900x600", label: "Small Desktop (900x600)" },
+											{ value: "768x1024", label: "Tablet (768x1024)" },
+											{ value: "360x640", label: "Mobile (360x640)" },
+										]}
+									/>
+								</div>
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Select the viewport size for browser interactions. This affects how websites are
+									displayed and interacted with.
+								</p>
+							</div>
+							<div>
+								<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
+									<span className="font-medium">Screenshot quality</span>
+									<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
+										<input
+											type="range"
+											min="1"
+											max="100"
+											step="1"
+											value={screenshotQuality ?? 75}
+											className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+											onChange={(e) =>
+												setCachedStateField("screenshotQuality", parseInt(e.target.value))
+											}
+										/>
+										<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
+									</div>
+								</div>
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Adjust the WebP quality of browser screenshots. Higher values provide clearer
+									screenshots but increase token usage.
+								</p>
+							</div>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 82 - 0
webview-ui/src/components/settings/CheckpointSettings.tsx

@@ -0,0 +1,82 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox, VSCodeRadio, VSCodeRadioGroup } from "@vscode/webview-ui-toolkit/react"
+import { GitBranch } from "lucide-react"
+
+import { CheckpointStorage, isCheckpointStorage } from "../../../../src/shared/checkpoints"
+
+import { SetCachedStateField } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type CheckpointSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	enableCheckpoints?: boolean
+	checkpointStorage?: CheckpointStorage
+	setCachedStateField: SetCachedStateField<"enableCheckpoints" | "checkpointStorage">
+}
+
+export const CheckpointSettings = ({
+	enableCheckpoints,
+	checkpointStorage = "task",
+	setCachedStateField,
+	...props
+}: CheckpointSettingsProps) => {
+	return (
+		<div {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<GitBranch className="w-4" />
+					<div>Checkpoints</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={enableCheckpoints}
+						onChange={(e: any) => {
+							setCachedStateField("enableCheckpoints", e.target.checked)
+						}}>
+						<span className="font-medium">Enable automatic checkpoints</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will automatically create checkpoints during task execution, making it easy to
+						review changes or revert to earlier states.
+					</p>
+					{enableCheckpoints && (
+						<div>
+							<div className="font-medium">Storage</div>
+							<VSCodeRadioGroup
+								role="radiogroup"
+								value={checkpointStorage}
+								onChange={(e) => {
+									if ("target" in e) {
+										const { value } = e.target as HTMLInputElement
+
+										if (isCheckpointStorage(value)) {
+											setCachedStateField("checkpointStorage", value)
+										}
+									}
+								}}>
+								<VSCodeRadio value="task">Task</VSCodeRadio>
+								<VSCodeRadio value="workspace">Workspace</VSCodeRadio>
+							</VSCodeRadioGroup>
+							{checkpointStorage === "task" && (
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Each task will have it's own dedicated git repository for storing checkpoints. This
+									provides the best isolation between tasks but uses more disk space.
+								</p>
+							)}
+							{checkpointStorage === "workspace" && (
+								<p className="text-vscode-descriptionForeground text-sm mt-0">
+									Each VSCode workspace will have it's own dedicated git repository for storing
+									checkpoints and tasks within a workspace will share this repository. This option
+									provides better performance and disk space efficiency.
+								</p>
+							)}
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 10 - 21
webview-ui/src/components/settings/ExperimentalFeature.tsx

@@ -7,25 +7,14 @@ interface ExperimentalFeatureProps {
 	onChange: (value: boolean) => void
 }
 
-const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => {
-	return (
-		<div>
-			<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-				<span style={{ color: "var(--vscode-errorForeground)" }}>⚠️</span>
-				<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
-					<span style={{ fontWeight: "500" }}>{name}</span>
-				</VSCodeCheckbox>
-			</div>
-			<p
-				style={{
-					fontSize: "12px",
-					marginBottom: 15,
-					color: "var(--vscode-descriptionForeground)",
-				}}>
-				{description}
-			</p>
+export const ExperimentalFeature = ({ name, description, enabled, onChange }: ExperimentalFeatureProps) => (
+	<div>
+		<div className="flex items-center gap-2">
+			<span className="text-vscode-errorForeground">⚠️</span>
+			<VSCodeCheckbox checked={enabled} onChange={(e: any) => onChange(e.target.checked)}>
+				<span className="font-medium">{name}</span>
+			</VSCodeCheckbox>
 		</div>
-	)
-}
-
-export default ExperimentalFeature
+		<p className="text-vscode-descriptionForeground text-sm mt-0">{description}</p>
+	</div>
+)

+ 53 - 0
webview-ui/src/components/settings/ExperimentalSettings.tsx

@@ -0,0 +1,53 @@
+import { HTMLAttributes } from "react"
+import { FlaskConical } from "lucide-react"
+
+import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
+
+import { cn } from "@/lib/utils"
+
+import { SetCachedStateField, SetExperimentEnabled } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+import { ExperimentalFeature } from "./ExperimentalFeature"
+
+type ExperimentalSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	setCachedStateField: SetCachedStateField<
+		"rateLimitSeconds" | "terminalOutputLineLimit" | "maxOpenTabsContext" | "diffEnabled" | "fuzzyMatchThreshold"
+	>
+	experiments: Record<ExperimentId, boolean>
+	setExperimentEnabled: SetExperimentEnabled
+}
+
+export const ExperimentalSettings = ({
+	setCachedStateField,
+	experiments,
+	setExperimentEnabled,
+	className,
+	...props
+}: ExperimentalSettingsProps) => {
+	return (
+		<div className={cn("flex flex-col gap-2", className)} {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<FlaskConical className="w-4" />
+					<div>Experimental Features</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				{Object.entries(experimentConfigsMap)
+					.filter((config) => config[0] !== "DIFF_STRATEGY")
+					.map((config) => (
+						<ExperimentalFeature
+							key={config[0]}
+							{...config[1]}
+							enabled={experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false}
+							onChange={(enabled) =>
+								setExperimentEnabled(EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS], enabled)
+							}
+						/>
+					))}
+			</Section>
+		</div>
+	)
+}

+ 69 - 0
webview-ui/src/components/settings/NotificationSettings.tsx

@@ -0,0 +1,69 @@
+import { HTMLAttributes } from "react"
+import { VSCodeCheckbox } from "@vscode/webview-ui-toolkit/react"
+import { Bell } from "lucide-react"
+
+import { SetCachedStateField } from "./types"
+import { SectionHeader } from "./SectionHeader"
+import { Section } from "./Section"
+
+type NotificationSettingsProps = HTMLAttributes<HTMLDivElement> & {
+	soundEnabled?: boolean
+	soundVolume?: number
+	setCachedStateField: SetCachedStateField<"soundEnabled" | "soundVolume">
+}
+
+export const NotificationSettings = ({
+	soundEnabled,
+	soundVolume,
+	setCachedStateField,
+	...props
+}: NotificationSettingsProps) => {
+	return (
+		<div {...props}>
+			<SectionHeader>
+				<div className="flex items-center gap-2">
+					<Bell className="w-4" />
+					<div>Notifications</div>
+				</div>
+			</SectionHeader>
+
+			<Section>
+				<div>
+					<VSCodeCheckbox
+						checked={soundEnabled}
+						onChange={(e: any) => setCachedStateField("soundEnabled", e.target.checked)}>
+						<span className="font-medium">Enable sound effects</span>
+					</VSCodeCheckbox>
+					<p className="text-vscode-descriptionForeground text-sm mt-0">
+						When enabled, Roo will play sound effects for notifications and events.
+					</p>
+					{soundEnabled && (
+						<div
+							style={{
+								marginLeft: 0,
+								paddingLeft: 10,
+								borderLeft: "2px solid var(--vscode-button-background)",
+							}}>
+							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
+								<input
+									type="range"
+									min="0"
+									max="1"
+									step="0.01"
+									value={soundVolume ?? 0.5}
+									onChange={(e) => setCachedStateField("soundVolume", parseFloat(e.target.value))}
+									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
+									aria-label="Volume"
+								/>
+								<span style={{ minWidth: "35px", textAlign: "left" }}>
+									{((soundVolume ?? 0.5) * 100).toFixed(0)}%
+								</span>
+							</div>
+							<p className="text-vscode-descriptionForeground text-sm mt-1">Volume</p>
+						</div>
+					)}
+				</div>
+			</Section>
+		</div>
+	)
+}

+ 9 - 0
webview-ui/src/components/settings/Section.tsx

@@ -0,0 +1,9 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type SectionProps = HTMLAttributes<HTMLDivElement>
+
+export const Section = ({ className, ...props }: SectionProps) => (
+	<div className={cn("flex flex-col gap-2 p-5", className)} {...props} />
+)

+ 15 - 0
webview-ui/src/components/settings/SectionHeader.tsx

@@ -0,0 +1,15 @@
+import { HTMLAttributes } from "react"
+
+import { cn } from "@/lib/utils"
+
+type SectionHeaderProps = HTMLAttributes<HTMLDivElement> & {
+	children: React.ReactNode
+	description?: string
+}
+
+export const SectionHeader = ({ description, children, className, ...props }: SectionHeaderProps) => (
+	<div className={cn("sticky top-0 z-10 bg-vscode-panel-border px-5 py-4", className)} {...props}>
+		<h4 className="m-0">{children}</h4>
+		{description && <p className="text-vscode-descriptionForeground text-sm mt-2 mb-0">{description}</p>}
+	</div>
+)

+ 36 - 0
webview-ui/src/components/settings/SettingsFooter.tsx

@@ -0,0 +1,36 @@
+import { HTMLAttributes } from "react"
+
+import { VSCodeButton, VSCodeLink } from "@vscode/webview-ui-toolkit/react"
+
+import { vscode } from "@/utils/vscode"
+import { cn } from "@/lib/utils"
+
+type SettingsFooterProps = HTMLAttributes<HTMLDivElement> & {
+	version: string
+}
+
+export const SettingsFooter = ({ version, className, ...props }: SettingsFooterProps) => (
+	<div className={cn("text-vscode-descriptionForeground p-5", className)} {...props}>
+		<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
+			If you have any questions or feedback, feel free to open an issue at{" "}
+			<VSCodeLink href="https://github.com/RooVetGit/Roo-Code" style={{ display: "inline" }}>
+				github.com/RooVetGit/Roo-Code
+			</VSCodeLink>{" "}
+			or join{" "}
+			<VSCodeLink href="https://www.reddit.com/r/RooCode/" style={{ display: "inline" }}>
+				reddit.com/r/RooCode
+			</VSCodeLink>
+		</p>
+		<p className="italic">Roo Code v{version}</p>
+		<div className="flex justify-between items-center gap-3">
+			<p>Reset all global state and secret storage in the extension.</p>
+			<VSCodeButton
+				onClick={() => vscode.postMessage({ type: "resetState" })}
+				appearance="secondary"
+				className="shrink-0">
+				<span className="codicon codicon-warning text-vscode-errorForeground mr-1" />
+				Reset
+			</VSCodeButton>
+		</div>
+	</div>
+)

+ 224 - 693
webview-ui/src/components/settings/SettingsView.tsx

@@ -1,7 +1,12 @@
 import { forwardRef, memo, useCallback, useEffect, useImperativeHandle, useMemo, useRef, useState } from "react"
-import { VSCodeButton, VSCodeCheckbox, VSCodeLink, VSCodeTextField } from "@vscode/webview-ui-toolkit/react"
-import { Button, Dropdown, type DropdownOption } from "vscrui"
+import { Button as VSCodeButton } from "vscrui"
+import { CheckCheck, SquareMousePointer, Webhook, GitBranch, Bell, Cog, FlaskConical } from "lucide-react"
 
+import { ExperimentId } from "../../../../src/shared/experiments"
+import { ApiConfiguration } from "../../../../src/shared/api"
+
+import { vscode } from "@/utils/vscode"
+import { ExtensionStateContextType, useExtensionState } from "@/context/ExtensionStateContext"
 import {
 	AlertDialog,
 	AlertDialogContent,
@@ -11,37 +16,43 @@ import {
 	AlertDialogAction,
 	AlertDialogHeader,
 	AlertDialogFooter,
+	Button,
 } from "@/components/ui"
 
-import { vscode } from "../../utils/vscode"
-import { ExtensionStateContextType, useExtensionState } from "../../context/ExtensionStateContext"
-import { EXPERIMENT_IDS, experimentConfigsMap, ExperimentId } from "../../../../src/shared/experiments"
-import { ApiConfiguration } from "../../../../src/shared/api"
-
-import ExperimentalFeature from "./ExperimentalFeature"
+import { SetCachedStateField, SetExperimentEnabled } from "./types"
+import { SectionHeader } from "./SectionHeader"
 import ApiConfigManager from "./ApiConfigManager"
 import ApiOptions from "./ApiOptions"
-
-type SettingsViewProps = {
-	onDone: () => void
-}
+import { AutoApproveSettings } from "./AutoApproveSettings"
+import { BrowserSettings } from "./BrowserSettings"
+import { CheckpointSettings } from "./CheckpointSettings"
+import { NotificationSettings } from "./NotificationSettings"
+import { AdvancedSettings } from "./AdvancedSettings"
+import { SettingsFooter } from "./SettingsFooter"
+import { Section } from "./Section"
+import { ExperimentalSettings } from "./ExperimentalSettings"
 
 export interface SettingsViewRef {
 	checkUnsaveChanges: (then: () => void) => void
 }
 
+type SettingsViewProps = {
+	onDone: () => void
+}
+
 const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone }, ref) => {
 	const extensionState = useExtensionState()
-	const [commandInput, setCommandInput] = useState("")
+	const { currentApiConfigName, listApiConfigMeta, uriScheme, version } = extensionState
+
 	const [isDiscardDialogShow, setDiscardDialogShow] = useState(false)
-	const [cachedState, setCachedState] = useState(extensionState)
 	const [isChangeDetected, setChangeDetected] = useState(false)
-	const prevApiConfigName = useRef(extensionState.currentApiConfigName)
-	const confirmDialogHandler = useRef<() => void>()
 	const [errorMessage, setErrorMessage] = useState<string | undefined>(undefined)
 
-	// TODO: Reduce WebviewMessage/ExtensionState complexity
-	const { currentApiConfigName } = extensionState
+	const prevApiConfigName = useRef(currentApiConfigName)
+	const confirmDialogHandler = useRef<() => void>()
+
+	const [cachedState, setCachedState] = useState(extensionState)
+
 	const {
 		alwaysAllowReadOnly,
 		allowedCommands,
@@ -54,6 +65,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		browserToolEnabled,
 		browserViewportSize,
 		enableCheckpoints,
+		checkpointStorage,
 		diffEnabled,
 		experiments,
 		fuzzyMatchThreshold,
@@ -68,7 +80,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		writeDelayMs,
 	} = cachedState
 
-	//Make sure apiConfiguration is initialized and managed by SettingsView
+	// Make sure apiConfiguration is initialized and managed by SettingsView.
 	const apiConfiguration = useMemo(() => cachedState.apiConfiguration ?? {}, [cachedState.apiConfiguration])
 
 	useEffect(() => {
@@ -80,24 +92,19 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 
 		setCachedState((prevCachedState) => ({ ...prevCachedState, ...extensionState }))
 		prevApiConfigName.current = currentApiConfigName
-		// console.log("useEffect: currentApiConfigName changed, setChangeDetected -> false")
 		setChangeDetected(false)
 	}, [currentApiConfigName, extensionState, isChangeDetected])
 
-	const setCachedStateField = useCallback(
-		<K extends keyof ExtensionStateContextType>(field: K, value: ExtensionStateContextType[K]) => {
-			setCachedState((prevState) => {
-				if (prevState[field] === value) {
-					return prevState
-				}
+	const setCachedStateField: SetCachedStateField<keyof ExtensionStateContextType> = useCallback((field, value) => {
+		setCachedState((prevState) => {
+			if (prevState[field] === value) {
+				return prevState
+			}
 
-				// console.log(`setCachedStateField(${field} -> ${value}): setChangeDetected -> true`)
-				setChangeDetected(true)
-				return { ...prevState, [field]: value }
-			})
-		},
-		[],
-	)
+			setChangeDetected(true)
+			return { ...prevState, [field]: value }
+		})
+	}, [])
 
 	const setApiConfigurationField = useCallback(
 		<K extends keyof ApiConfiguration>(field: K, value: ApiConfiguration[K]) => {
@@ -106,7 +113,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 					return prevState
 				}
 
-				// console.log(`setApiConfigurationField(${field} -> ${value}): setChangeDetected -> true`)
 				setChangeDetected(true)
 
 				return { ...prevState, apiConfiguration: { ...prevState.apiConfiguration, [field]: value } }
@@ -115,13 +121,12 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		[],
 	)
 
-	const setExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => {
+	const setExperimentEnabled: SetExperimentEnabled = useCallback((id: ExperimentId, enabled: boolean) => {
 		setCachedState((prevState) => {
 			if (prevState.experiments?.[id] === enabled) {
 				return prevState
 			}
 
-			// console.log("setExperimentEnabled: setChangeDetected -> true")
 			setChangeDetected(true)
 
 			return {
@@ -146,6 +151,7 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 			vscode.postMessage({ type: "soundVolume", value: soundVolume })
 			vscode.postMessage({ type: "diffEnabled", bool: diffEnabled })
 			vscode.postMessage({ type: "enableCheckpoints", bool: enableCheckpoints })
+			vscode.postMessage({ type: "checkpointStorage", text: checkpointStorage })
 			vscode.postMessage({ type: "browserViewportSize", text: browserViewportSize })
 			vscode.postMessage({ type: "fuzzyMatchThreshold", value: fuzzyMatchThreshold ?? 1.0 })
 			vscode.postMessage({ type: "writeDelayMs", value: writeDelayMs })
@@ -160,7 +166,6 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 			vscode.postMessage({ type: "updateExperimental", values: experiments })
 			vscode.postMessage({ type: "alwaysAllowModeSwitch", bool: alwaysAllowModeSwitch })
 			vscode.postMessage({ type: "upsertApiConfiguration", text: currentApiConfigName, apiConfiguration })
-			// console.log("handleSubmit: setChangeDetected -> false")
 			setChangeDetected(false)
 		}
 	}
@@ -185,108 +190,125 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 		}
 	}, [])
 
-	const handleResetState = () => {
-		vscode.postMessage({ type: "resetState" })
-	}
+	const providersRef = useRef<HTMLDivElement>(null)
+	const autoApproveRef = useRef<HTMLDivElement>(null)
+	const browserRef = useRef<HTMLDivElement>(null)
+	const checkpointRef = useRef<HTMLDivElement>(null)
+	const notificationsRef = useRef<HTMLDivElement>(null)
+	const advancedRef = useRef<HTMLDivElement>(null)
+	const experimentalRef = useRef<HTMLDivElement>(null)
+
+	const [activeSection, setActiveSection] = useState<string>("providers")
+
+	const sections = useMemo(
+		() => [
+			{ id: "providers", icon: Webhook, ref: providersRef },
+			{ id: "autoApprove", icon: CheckCheck, ref: autoApproveRef },
+			{ id: "browser", icon: SquareMousePointer, ref: browserRef },
+			{ id: "checkpoint", icon: GitBranch, ref: checkpointRef },
+			{ id: "notifications", icon: Bell, ref: notificationsRef },
+			{ id: "advanced", icon: Cog, ref: advancedRef },
+			{ id: "experimental", icon: FlaskConical, ref: experimentalRef },
+		],
+		[providersRef, autoApproveRef, browserRef, checkpointRef, notificationsRef, advancedRef, experimentalRef],
+	)
 
-	const handleAddCommand = () => {
-		const currentCommands = allowedCommands ?? []
-		if (commandInput && !currentCommands.includes(commandInput)) {
-			const newCommands = [...currentCommands, commandInput]
-			setCachedStateField("allowedCommands", newCommands)
-			setCommandInput("")
-			vscode.postMessage({ type: "allowedCommands", commands: newCommands })
+	const handleScroll = useCallback((e: React.UIEvent<HTMLDivElement>) => {
+		const sections = [
+			{ ref: providersRef, id: "providers" },
+			{ ref: autoApproveRef, id: "autoApprove" },
+			{ ref: browserRef, id: "browser" },
+			{ ref: checkpointRef, id: "checkpoint" },
+			{ ref: notificationsRef, id: "notifications" },
+			{ ref: advancedRef, id: "advanced" },
+			{ ref: experimentalRef, id: "experimental" },
+		]
+
+		for (const section of sections) {
+			const element = section.ref.current
+
+			if (element) {
+				const { top } = element.getBoundingClientRect()
+
+				if (top >= 0 && top <= 50) {
+					setActiveSection(section.id)
+					break
+				}
+			}
 		}
-	}
+	}, [])
 
-	const sliderLabelStyle = {
-		minWidth: "45px",
-		textAlign: "right" as const,
-		lineHeight: "20px",
-		paddingBottom: "2px",
-	}
+	const scrollToSection = (ref: React.RefObject<HTMLDivElement>) => ref.current?.scrollIntoView()
 
 	return (
-		<div
-			style={{
-				position: "fixed",
-				top: 0,
-				left: 0,
-				right: 0,
-				bottom: 0,
-				padding: "10px 0px 0px 20px",
-				display: "flex",
-				flexDirection: "column",
-				overflow: "hidden",
-			}}>
-			<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
-				<AlertDialogContent>
-					<AlertDialogHeader>
-						<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
-						<AlertDialogDescription>
-							<span className={`codicon codicon-warning align-middle mr-1`} />
-							Do you want to discard changes and continue?
-						</AlertDialogDescription>
-					</AlertDialogHeader>
-					<AlertDialogFooter>
-						<AlertDialogAction onClick={() => onConfirmDialogResult(true)}>Yes</AlertDialogAction>
-						<AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>No</AlertDialogCancel>
-					</AlertDialogFooter>
-				</AlertDialogContent>
-			</AlertDialog>
-			<div
-				style={{
-					display: "flex",
-					justifyContent: "space-between",
-					alignItems: "center",
-					marginBottom: "17px",
-					paddingRight: 17,
-				}}>
-				<h3 style={{ color: "var(--vscode-foreground)", margin: 0 }}>Settings</h3>
-				<div
-					style={{
-						display: "flex",
-						justifyContent: "space-between",
-						gap: "6px",
-					}}>
-					<Button
-						appearance={isSettingValid ? "primary" : "secondary"}
-						className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
-						title={!isSettingValid ? errorMessage : isChangeDetected ? "Save changes" : "Nothing changed"}
-						onClick={handleSubmit}
-						disabled={!isChangeDetected || !isSettingValid}>
-						Save
-					</Button>
-					<VSCodeButton
-						appearance="secondary"
-						title="Discard unsaved changes and close settings panel"
-						onClick={() => checkUnsaveChanges(onDone)}>
-						Done
-					</VSCodeButton>
+		<div className="fixed inset-0 flex flex-col overflow-hidden">
+			<div className="px-5 py-2.5 border-b border-vscode-panel-border">
+				<div className="flex flex-col">
+					<div className="flex justify-between items-center">
+						<div className="flex items-center gap-2">
+							<h3 className="text-vscode-foreground m-0">Settings</h3>
+							<div className="hidden [@media(min-width:430px)]:flex items-center">
+								{sections.map(({ id, icon: Icon, ref }) => (
+									<Button
+										key={id}
+										variant="ghost"
+										size="icon"
+										className={activeSection === id ? "opacity-100" : "opacity-40"}
+										onClick={() => scrollToSection(ref)}>
+										<Icon />
+									</Button>
+								))}
+							</div>
+						</div>
+						<div className="flex gap-2">
+							<VSCodeButton
+								appearance={isSettingValid ? "primary" : "secondary"}
+								className={!isSettingValid ? "!border-vscode-errorForeground" : ""}
+								title={
+									!isSettingValid
+										? errorMessage
+										: isChangeDetected
+											? "Save changes"
+											: "Nothing changed"
+								}
+								onClick={handleSubmit}
+								disabled={!isChangeDetected || !isSettingValid}>
+								Save
+							</VSCodeButton>
+							<VSCodeButton
+								appearance="secondary"
+								title="Discard unsaved changes and close settings panel"
+								onClick={() => checkUnsaveChanges(onDone)}>
+								Done
+							</VSCodeButton>
+						</div>
+					</div>
 				</div>
 			</div>
+
 			<div
-				style={{ flexGrow: 1, overflowY: "scroll", paddingRight: 8, display: "flex", flexDirection: "column" }}>
-				<div style={{ marginBottom: 40 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Provider Settings</h3>
-					<div style={{ marginBottom: 15 }}>
+				className="flex flex-col flex-1 overflow-auto divide-y divide-vscode-panel-border"
+				onScroll={handleScroll}>
+				<div ref={providersRef}>
+					<SectionHeader>
+						<div className="flex items-center gap-2">
+							<Webhook className="w-4" />
+							<div>Providers</div>
+						</div>
+					</SectionHeader>
+
+					<Section>
 						<ApiConfigManager
 							currentApiConfigName={currentApiConfigName}
-							listApiConfigMeta={extensionState.listApiConfigMeta}
-							onSelectConfig={(configName: string) => {
-								checkUnsaveChanges(() => {
-									vscode.postMessage({
-										type: "loadApiConfiguration",
-										text: configName,
-									})
-								})
-							}}
-							onDeleteConfig={(configName: string) => {
-								vscode.postMessage({
-									type: "deleteApiConfiguration",
-									text: configName,
-								})
-							}}
+							listApiConfigMeta={listApiConfigMeta}
+							onSelectConfig={(configName: string) =>
+								checkUnsaveChanges(() =>
+									vscode.postMessage({ type: "loadApiConfiguration", text: configName }),
+								)
+							}
+							onDeleteConfig={(configName: string) =>
+								vscode.postMessage({ type: "deleteApiConfiguration", text: configName })
+							}
 							onRenameConfig={(oldName: string, newName: string) => {
 								vscode.postMessage({
 									type: "renameApiConfiguration",
@@ -295,595 +317,104 @@ const SettingsView = forwardRef<SettingsViewRef, SettingsViewProps>(({ onDone },
 								})
 								prevApiConfigName.current = newName
 							}}
-							onUpsertConfig={(configName: string) => {
+							onUpsertConfig={(configName: string) =>
 								vscode.postMessage({
 									type: "upsertApiConfiguration",
 									text: configName,
 									apiConfiguration,
 								})
-							}}
+							}
 						/>
 						<ApiOptions
-							uriScheme={extensionState.uriScheme}
+							uriScheme={uriScheme}
 							apiConfiguration={apiConfiguration}
 							setApiConfigurationField={setApiConfigurationField}
 							errorMessage={errorMessage}
 							setErrorMessage={setErrorMessage}
 						/>
-					</div>
+					</Section>
 				</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 Roo 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 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowReadOnly}
-							onChange={(e: any) => setCachedStateField("alwaysAllowReadOnly", 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, Roo will automatically view directory contents and read files without
-							requiring you to click the Approve button.
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowWrite}
-							onChange={(e: any) => setCachedStateField("alwaysAllowWrite", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve write operations</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically create and edit files without requiring approval
-						</p>
-						{alwaysAllowWrite && (
-							<div
-								style={{
-									marginTop: 10,
-									paddingLeft: 10,
-									borderLeft: "2px solid var(--vscode-button-background)",
-								}}>
-								<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
-									<input
-										type="range"
-										min="0"
-										max="5000"
-										step="100"
-										value={writeDelayMs}
-										onChange={(e) => setCachedStateField("writeDelayMs", parseInt(e.target.value))}
-										className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-									/>
-									<span style={{ minWidth: "45px", textAlign: "left" }}>{writeDelayMs}ms</span>
-								</div>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Delay after writes to allow diagnostics to detect potential problems
-								</p>
-							</div>
-						)}
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowBrowser}
-							onChange={(e: any) => setCachedStateField("alwaysAllowBrowser", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve browser actions</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically perform browser actions without requiring approval
-							<br />
-							Note: Only applies when the model supports computer use
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysApproveResubmit}
-							onChange={(e: any) => setCachedStateField("alwaysApproveResubmit", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always retry failed API requests</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically retry failed API requests when server returns an error response
-						</p>
-						{alwaysApproveResubmit && (
-							<div
-								style={{
-									marginTop: 10,
-									paddingLeft: 10,
-									borderLeft: "2px solid var(--vscode-button-background)",
-								}}>
-								<div style={{ display: "flex", alignItems: "center", gap: "10px" }}>
-									<input
-										type="range"
-										min="5"
-										max="100"
-										step="1"
-										value={requestDelaySeconds}
-										onChange={(e) =>
-											setCachedStateField("requestDelaySeconds", parseInt(e.target.value))
-										}
-										className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-									/>
-									<span style={{ minWidth: "45px", textAlign: "left" }}>{requestDelaySeconds}s</span>
-								</div>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Delay before retrying the request
-								</p>
-							</div>
-						)}
-					</div>
-
-					<div style={{ marginBottom: 5 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowMcp}
-							onChange={(e: any) => setCachedStateField("alwaysAllowMcp", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve MCP tools</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Enable auto-approval of individual MCP tools in the MCP Servers view (requires both this
-							setting and the tool's individual "Always allow" checkbox)
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowModeSwitch}
-							onChange={(e: any) => setCachedStateField("alwaysAllowModeSwitch", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve mode switching & task creation</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically switch between different AI modes and create new tasks without requiring
-							approval
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={alwaysAllowExecute}
-							onChange={(e: any) => setCachedStateField("alwaysAllowExecute", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Always approve allowed execute operations</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Automatically execute allowed terminal commands without requiring approval
-						</p>
-
-						{alwaysAllowExecute && (
-							<div
-								style={{
-									marginTop: 10,
-									paddingLeft: 10,
-									borderLeft: "2px solid var(--vscode-button-background)",
-								}}>
-								<span style={{ fontWeight: "500" }}>Allowed Auto-Execute Commands</span>
-								<p
-									style={{
-										fontSize: "12px",
-										marginTop: "5px",
-										color: "var(--vscode-descriptionForeground)",
-									}}>
-									Command prefixes that can be auto-executed when "Always approve execute operations"
-									is enabled. Add * to allow all commands (use with caution).
-								</p>
-
-								<div style={{ display: "flex", gap: "5px", marginTop: "10px" }}>
-									<VSCodeTextField
-										value={commandInput}
-										onInput={(e: any) => setCommandInput(e.target.value)}
-										onKeyDown={(e: any) => {
-											if (e.key === "Enter") {
-												e.preventDefault()
-												handleAddCommand()
-											}
-										}}
-										placeholder="Enter command prefix (e.g., 'git ')"
-										style={{ flexGrow: 1 }}
-									/>
-									<VSCodeButton onClick={handleAddCommand}>Add</VSCodeButton>
-								</div>
-
-								<div
-									style={{
-										marginTop: "10px",
-										display: "flex",
-										flexWrap: "wrap",
-										gap: "5px",
-									}}>
-									{(allowedCommands ?? []).map((cmd, index) => (
-										<div
-											key={index}
-											className="border border-vscode-input-border bg-primary text-primary-foreground flex items-center gap-1 rounded-xs px-1.5 p-0.5">
-											<span>{cmd}</span>
-											<VSCodeButton
-												appearance="icon"
-												className="text-primary-foreground"
-												onClick={() => {
-													const newCommands = (allowedCommands ?? []).filter(
-														(_, i) => i !== index,
-													)
-													setCachedStateField("allowedCommands", newCommands)
-													vscode.postMessage({
-														type: "allowedCommands",
-														commands: newCommands,
-													})
-												}}>
-												<span className="codicon codicon-close" />
-											</VSCodeButton>
-										</div>
-									))}
-								</div>
-							</div>
-						)}
-					</div>
+				<div ref={autoApproveRef}>
+					<AutoApproveSettings
+						alwaysAllowReadOnly={alwaysAllowReadOnly}
+						alwaysAllowWrite={alwaysAllowWrite}
+						writeDelayMs={writeDelayMs}
+						alwaysAllowBrowser={alwaysAllowBrowser}
+						alwaysApproveResubmit={alwaysApproveResubmit}
+						requestDelaySeconds={requestDelaySeconds}
+						alwaysAllowMcp={alwaysAllowMcp}
+						alwaysAllowModeSwitch={alwaysAllowModeSwitch}
+						alwaysAllowExecute={alwaysAllowExecute}
+						allowedCommands={allowedCommands}
+						setCachedStateField={setCachedStateField}
+					/>
 				</div>
 
-				<div style={{ marginBottom: 40 }}>
-					<h3 style={{ color: "var(--vscode-foreground)", margin: "0 0 15px 0" }}>Browser Settings</h3>
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={browserToolEnabled}
-							onChange={(e: any) => setCachedStateField("browserToolEnabled", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Enable browser tool</span>
-						</VSCodeCheckbox>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							When enabled, Roo can use a browser to interact with websites when using models that support
-							computer use.
-						</p>
-					</div>
-					{browserToolEnabled && (
-						<div
-							style={{
-								marginLeft: 0,
-								paddingLeft: 10,
-								borderLeft: "2px solid var(--vscode-button-background)",
-							}}>
-							<div style={{ marginBottom: 15 }}>
-								<label style={{ fontWeight: "500", display: "block", marginBottom: 5 }}>
-									Viewport size
-								</label>
-								<div className="dropdown-container">
-									<Dropdown
-										value={browserViewportSize}
-										onChange={(value: unknown) => {
-											setCachedStateField("browserViewportSize", (value as DropdownOption).value)
-										}}
-										style={{ width: "100%" }}
-										options={[
-											{ value: "1280x800", label: "Large Desktop (1280x800)" },
-											{ value: "900x600", label: "Small Desktop (900x600)" },
-											{ value: "768x1024", label: "Tablet (768x1024)" },
-											{ value: "360x640", label: "Mobile (360x640)" },
-										]}
-									/>
-								</div>
-								<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", flexDirection: "column", gap: "5px" }}>
-									<span style={{ fontWeight: "500" }}>Screenshot quality</span>
-									<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-										<input
-											type="range"
-											min="1"
-											max="100"
-											step="1"
-											value={screenshotQuality ?? 75}
-											className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-											onChange={(e) =>
-												setCachedStateField("screenshotQuality", parseInt(e.target.value))
-											}
-										/>
-										<span style={{ ...sliderLabelStyle }}>{screenshotQuality ?? 75}%</span>
-									</div>
-								</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 ref={browserRef}>
+					<BrowserSettings
+						browserToolEnabled={browserToolEnabled}
+						browserViewportSize={browserViewportSize}
+						screenshotQuality={screenshotQuality}
+						setCachedStateField={setCachedStateField}
+					/>
 				</div>
 
-				<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) => setCachedStateField("soundEnabled", e.target.checked)}>
-							<span style={{ fontWeight: "500" }}>Enable sound effects</span>
-						</VSCodeCheckbox>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							When enabled, Roo will play sound effects for notifications and events.
-						</p>
-					</div>
-					{soundEnabled && (
-						<div
-							style={{
-								marginLeft: 0,
-								paddingLeft: 10,
-								borderLeft: "2px solid var(--vscode-button-background)",
-							}}>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<span style={{ fontWeight: "500", minWidth: "100px" }}>Volume</span>
-								<input
-									type="range"
-									min="0"
-									max="1"
-									step="0.01"
-									value={soundVolume ?? 0.5}
-									onChange={(e) => setCachedStateField("soundVolume", parseFloat(e.target.value))}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-									aria-label="Volume"
-								/>
-								<span style={{ minWidth: "35px", textAlign: "left" }}>
-									{((soundVolume ?? 0.5) * 100).toFixed(0)}%
-								</span>
-							</div>
-						</div>
-					)}
+				<div ref={checkpointRef}>
+					<CheckpointSettings
+						enableCheckpoints={enableCheckpoints}
+						checkpointStorage={checkpointStorage}
+						setCachedStateField={setCachedStateField}
+					/>
 				</div>
 
-				<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", flexDirection: "column", gap: "5px" }}>
-							<span style={{ fontWeight: "500" }}>Rate limit</span>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<input
-									type="range"
-									min="0"
-									max="60"
-									step="1"
-									value={rateLimitSeconds}
-									onChange={(e) => setCachedStateField("rateLimitSeconds", parseInt(e.target.value))}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-								/>
-								<span style={{ ...sliderLabelStyle }}>{rateLimitSeconds}s</span>
-							</div>
-						</div>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Minimum time between API requests.
-						</p>
-					</div>
-					<div style={{ marginBottom: 15 }}>
-						<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
-							<span style={{ fontWeight: "500" }}>Terminal output limit</span>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<input
-									type="range"
-									min="100"
-									max="5000"
-									step="100"
-									value={terminalOutputLineLimit ?? 500}
-									onChange={(e) =>
-										setCachedStateField("terminalOutputLineLimit", parseInt(e.target.value))
-									}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-								/>
-								<span style={{ ...sliderLabelStyle }}>{terminalOutputLineLimit ?? 500}</span>
-							</div>
-						</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: 15 }}>
-						<div style={{ display: "flex", flexDirection: "column", gap: "5px" }}>
-							<span style={{ fontWeight: "500" }}>Open tabs context limit</span>
-							<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-								<input
-									type="range"
-									min="0"
-									max="500"
-									step="1"
-									value={maxOpenTabsContext ?? 20}
-									onChange={(e) =>
-										setCachedStateField("maxOpenTabsContext", parseInt(e.target.value))
-									}
-									className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-								/>
-								<span style={{ ...sliderLabelStyle }}>{maxOpenTabsContext ?? 20}</span>
-							</div>
-						</div>
-						<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
-							Maximum number of VSCode open tabs to include in context. Higher values provide more context
-							but increase token usage.
-						</p>
-					</div>
-
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={enableCheckpoints}
-							onChange={(e: any) => {
-								setCachedStateField("enableCheckpoints", e.target.checked)
-							}}>
-							<span style={{ fontWeight: "500" }}>Enable automatic checkpoints</span>
-						</VSCodeCheckbox>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							When enabled, Roo will automatically create checkpoints during task execution, making it
-							easy to review changes or revert to earlier states.
-						</p>
-					</div>
+				<div ref={notificationsRef}>
+					<NotificationSettings
+						soundEnabled={soundEnabled}
+						soundVolume={soundVolume}
+						setCachedStateField={setCachedStateField}
+					/>
+				</div>
 
-					<div style={{ marginBottom: 15 }}>
-						<VSCodeCheckbox
-							checked={diffEnabled}
-							onChange={(e: any) => {
-								setCachedStateField("diffEnabled", e.target.checked)
-								if (!e.target.checked) {
-									// Reset experimental strategy when diffs are disabled
-									setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, false)
-								}
-							}}>
-							<span style={{ fontWeight: "500" }}>Enable editing through diffs</span>
-						</VSCodeCheckbox>
-						<p
-							style={{
-								fontSize: "12px",
-								marginTop: "5px",
-								color: "var(--vscode-descriptionForeground)",
-							}}>
-							When enabled, Roo will be able to edit files more quickly and will automatically reject
-							truncated full-file writes. Works best with the latest Claude 3.7 Sonnet model.
-						</p>
-
-						{diffEnabled && (
-							<div style={{ marginTop: 10 }}>
-								<div
-									style={{
-										display: "flex",
-										flexDirection: "column",
-										gap: "5px",
-										marginTop: "10px",
-										marginBottom: "10px",
-										paddingLeft: "10px",
-										borderLeft: "2px solid var(--vscode-button-background)",
-									}}>
-									<span style={{ fontWeight: "500" }}>Match precision</span>
-									<div style={{ display: "flex", alignItems: "center", gap: "5px" }}>
-										<input
-											type="range"
-											min="0.8"
-											max="1"
-											step="0.005"
-											value={fuzzyMatchThreshold ?? 1.0}
-											onChange={(e) => {
-												setCachedStateField("fuzzyMatchThreshold", parseFloat(e.target.value))
-											}}
-											className="h-2 focus:outline-0 w-4/5 accent-vscode-button-background"
-										/>
-										<span style={{ ...sliderLabelStyle }}>
-											{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>
-									<ExperimentalFeature
-										key={EXPERIMENT_IDS.DIFF_STRATEGY}
-										{...experimentConfigsMap.DIFF_STRATEGY}
-										enabled={experiments[EXPERIMENT_IDS.DIFF_STRATEGY] ?? false}
-										onChange={(enabled) =>
-											setExperimentEnabled(EXPERIMENT_IDS.DIFF_STRATEGY, enabled)
-										}
-									/>
-								</div>
-							</div>
-						)}
-
-						{Object.entries(experimentConfigsMap)
-							.filter((config) => config[0] !== "DIFF_STRATEGY")
-							.map((config) => (
-								<ExperimentalFeature
-									key={config[0]}
-									{...config[1]}
-									enabled={
-										experiments[EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS]] ?? false
-									}
-									onChange={(enabled) =>
-										setExperimentEnabled(
-											EXPERIMENT_IDS[config[0] as keyof typeof EXPERIMENT_IDS],
-											enabled,
-										)
-									}
-								/>
-							))}
-					</div>
+				<div ref={advancedRef}>
+					<AdvancedSettings
+						rateLimitSeconds={rateLimitSeconds}
+						terminalOutputLineLimit={terminalOutputLineLimit}
+						maxOpenTabsContext={maxOpenTabsContext}
+						diffEnabled={diffEnabled}
+						fuzzyMatchThreshold={fuzzyMatchThreshold}
+						setCachedStateField={setCachedStateField}
+						setExperimentEnabled={setExperimentEnabled}
+						experiments={experiments}
+					/>
 				</div>
 
-				<div
-					style={{
-						textAlign: "center",
-						color: "var(--vscode-descriptionForeground)",
-						fontSize: "12px",
-						lineHeight: "1.2",
-						marginTop: "auto",
-						padding: "10px 8px 15px 0px",
-					}}>
-					<p style={{ wordWrap: "break-word", margin: 0, padding: 0 }}>
-						If you have any questions or feedback, feel free to open an issue at{" "}
-						<VSCodeLink href="https://github.com/RooVetGit/Roo-Code" style={{ display: "inline" }}>
-							github.com/RooVetGit/Roo-Code
-						</VSCodeLink>{" "}
-						or join{" "}
-						<VSCodeLink href="https://www.reddit.com/r/RooCode/" style={{ display: "inline" }}>
-							reddit.com/r/RooCode
-						</VSCodeLink>
-					</p>
-					<p style={{ fontStyle: "italic", margin: "10px 0 0 0", padding: 0, marginBottom: 100 }}>
-						v{extensionState.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 ref={experimentalRef}>
+					<ExperimentalSettings
+						setCachedStateField={setCachedStateField}
+						setExperimentEnabled={setExperimentEnabled}
+						experiments={experiments}
+					/>
 				</div>
+
+				<SettingsFooter version={version} />
 			</div>
+
+			<AlertDialog open={isDiscardDialogShow} onOpenChange={setDiscardDialogShow}>
+				<AlertDialogContent>
+					<AlertDialogHeader>
+						<AlertDialogTitle>Unsaved changes</AlertDialogTitle>
+						<AlertDialogDescription>
+							<span className={`codicon codicon-warning align-middle mr-1`} />
+							Do you want to discard changes and continue?
+						</AlertDialogDescription>
+					</AlertDialogHeader>
+					<AlertDialogFooter>
+						<AlertDialogAction onClick={() => onConfirmDialogResult(true)}>Yes</AlertDialogAction>
+						<AlertDialogCancel onClick={() => onConfirmDialogResult(false)}>No</AlertDialogCancel>
+					</AlertDialogFooter>
+				</AlertDialogContent>
+			</AlertDialog>
 		</div>
 	)
 })

+ 3 - 3
webview-ui/src/components/settings/TemperatureControl.tsx

@@ -32,10 +32,10 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
 						setInputValue(value ?? 0) // Use the value from apiConfiguration, if set
 					}
 				}}>
-				<span style={{ fontWeight: "500" }}>Use custom temperature</span>
+				<span className="font-medium">Use custom temperature</span>
 			</VSCodeCheckbox>
 
-			<p style={{ fontSize: "12px", marginTop: "5px", color: "var(--vscode-descriptionForeground)" }}>
+			<p className="text-vscode-descriptionForeground text-sm mt-0">
 				Controls randomness in the model's responses.
 			</p>
 
@@ -59,7 +59,7 @@ export const TemperatureControl = ({ value, onChange, maxValue = 1 }: Temperatur
 						/>
 						<span>{inputValue}</span>
 					</div>
-					<p style={{ fontSize: "12px", marginTop: "8px", color: "var(--vscode-descriptionForeground)" }}>
+					<p className="text-vscode-descriptionForeground text-sm mt-1">
 						Higher values make output more random, lower values make it more deterministic.
 					</p>
 				</div>

+ 18 - 0
webview-ui/src/components/settings/__tests__/SettingsView.test.tsx

@@ -1,3 +1,5 @@
+// npx jest src/components/settings/__tests__/SettingsView.test.ts
+
 import { render, screen, fireEvent } from "@testing-library/react"
 import SettingsView from "../SettingsView"
 import { ExtensionStateContextProvider } from "../../../context/ExtensionStateContext"
@@ -10,6 +12,22 @@ jest.mock("../../../utils/vscode", () => ({
 	},
 }))
 
+// Mock all lucide-react icons with a proxy to handle any icon requested
+jest.mock("lucide-react", () => {
+	return new Proxy(
+		{},
+		{
+			get: function (obj, prop) {
+				// Return a component factory for any icon that's requested
+				if (prop === "__esModule") {
+					return true
+				}
+				return () => <div data-testid={`${String(prop)}-icon`}>{String(prop)}</div>
+			},
+		},
+	)
+})
+
 // Mock ApiConfigManager component
 jest.mock("../ApiConfigManager", () => ({
 	__esModule: true,

+ 7 - 2
webview-ui/src/components/settings/styles.ts

@@ -1,7 +1,5 @@
 import styled from "styled-components"
 
-export const DROPDOWN_Z_INDEX = 1_000
-
 export const DropdownWrapper = styled.div`
 	position: relative;
 	width: 100%;
@@ -78,3 +76,10 @@ export const StyledMarkdown = styled.div`
 		}
 	}
 `
+
+export const sliderLabelStyle = {
+	minWidth: "45px",
+	textAlign: "right" as const,
+	lineHeight: "20px",
+	paddingBottom: "2px",
+}

+ 10 - 0
webview-ui/src/components/settings/types.ts

@@ -0,0 +1,10 @@
+import { ExperimentId } from "../../../../src/shared/experiments"
+
+import { ExtensionStateContextType } from "@/context/ExtensionStateContext"
+
+export type SetCachedStateField<K extends keyof ExtensionStateContextType> = (
+	field: K,
+	value: ExtensionStateContextType[K],
+) => void
+
+export type SetExperimentEnabled = (id: ExperimentId, enabled: boolean) => void

+ 15 - 14
webview-ui/src/components/ui/__tests__/select-dropdown.test.tsx

@@ -115,6 +115,21 @@ describe("SelectDropdown", () => {
 		expect(trigger.classList.toString()).toContain("custom-trigger-class")
 	})
 
+	it("ensures open state is controlled via props", () => {
+		// Test that the component accepts and uses the open state controlled prop
+		render(<SelectDropdown value="option1" options={options} onChange={onChangeMock} />)
+
+		// The component should render the dropdown root with correct props
+		const dropdown = screen.getByTestId("dropdown-root")
+		expect(dropdown).toBeInTheDocument()
+
+		// Verify trigger and content are rendered
+		const trigger = screen.getByTestId("dropdown-trigger")
+		const content = screen.getByTestId("dropdown-content")
+		expect(trigger).toBeInTheDocument()
+		expect(content).toBeInTheDocument()
+	})
+
 	// Tests for the new functionality
 	describe("Option types", () => {
 		it("renders separator options correctly", () => {
@@ -131,20 +146,6 @@ describe("SelectDropdown", () => {
 			expect(separators.length).toBe(1)
 		})
 
-		it("renders string separator (backward compatibility) correctly", () => {
-			const optionsWithStringSeparator = [
-				{ value: "option1", label: "Option 1" },
-				{ value: "sep-1", label: "────", disabled: true },
-				{ value: "option2", label: "Option 2" },
-			]
-
-			render(<SelectDropdown value="option1" options={optionsWithStringSeparator} onChange={onChangeMock} />)
-
-			// Check for separator
-			const separators = screen.getAllByTestId("dropdown-separator")
-			expect(separators.length).toBe(1)
-		})
-
 		it("renders shortcut options correctly", () => {
 			const shortcutText = "Ctrl+K"
 			const optionsWithShortcut = [

+ 21 - 2
webview-ui/src/components/ui/select-dropdown.tsx

@@ -7,6 +7,7 @@ import {
 	DropdownMenuSeparator,
 } from "./dropdown-menu"
 import { cn } from "@/lib/utils"
+import { useEffect, useState } from "react"
 
 // Constants for option types
 export enum DropdownOptionType {
@@ -57,6 +58,19 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 		},
 		ref,
 	) => {
+		// Track open state
+		const [open, setOpen] = React.useState(false)
+		const [portalContainer, setPortalContainer] = useState<HTMLElement>()
+
+		useEffect(() => {
+			// The dropdown menu uses a portal from @shadcn/ui which by default renders
+			// at the document root. This causes the menu to remain visible even when
+			// the parent ChatView component is hidden (during settings/history view).
+			// By moving the portal inside ChatView, the menu will properly hide when
+			// its parent is hidden.
+			setPortalContainer(document.getElementById("chat-view-portal") || undefined)
+		}, [])
+
 		// Find the selected option label
 		const selectedOption = options.find((option) => option.value === value)
 		const displayText = selectedOption?.label || placeholder || ""
@@ -69,13 +83,15 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 					type: "action",
 					action: option.value,
 				})
+				setOpen(false)
 				return
 			}
 			onChange(option.value)
+			setOpen(false)
 		}
 
 		return (
-			<DropdownMenu>
+			<DropdownMenu open={open} onOpenChange={setOpen}>
 				<DropdownMenuTrigger
 					ref={ref}
 					disabled={disabled}
@@ -112,13 +128,16 @@ export const SelectDropdown = React.forwardRef<React.ElementRef<typeof DropdownM
 				<DropdownMenuContent
 					align={align}
 					sideOffset={sideOffset}
+					onEscapeKeyDown={() => setOpen(false)}
+					onInteractOutside={() => setOpen(false)}
+					container={portalContainer}
 					className={cn(
 						"bg-vscode-dropdown-background text-vscode-dropdown-foreground border border-vscode-dropdown-border z-50",
 						contentClassName,
 					)}>
 					{options.map((option, index) => {
 						// Handle separator type
-						if (option.type === DropdownOptionType.SEPARATOR || option.label.includes("────")) {
+						if (option.type === DropdownOptionType.SEPARATOR) {
 							return <DropdownMenuSeparator key={`sep-${index}`} />
 						}
 

+ 1 - 0
webview-ui/src/context/ExtensionStateContext.tsx

@@ -107,6 +107,7 @@ export const ExtensionStateContextProvider: React.FC<{ children: React.ReactNode
 		soundVolume: 0.5,
 		diffEnabled: false,
 		enableCheckpoints: true,
+		checkpointStorage: "task",
 		fuzzyMatchThreshold: 1.0,
 		preferredLanguage: "English",
 		writeDelayMs: 1000,

+ 1 - 0
webview-ui/src/context/__tests__/ExtensionStateContext.test.tsx

@@ -79,6 +79,7 @@ describe("mergeExtensionState", () => {
 			taskHistory: [],
 			shouldShowAnnouncement: false,
 			enableCheckpoints: true,
+			checkpointStorage: "task",
 			preferredLanguage: "English",
 			writeDelayMs: 1000,
 			requestDelaySeconds: 5,

+ 4 - 0
webview-ui/src/index.css

@@ -96,6 +96,10 @@
 	--color-vscode-list-hoverForeground: var(--vscode-list-hoverForeground);
 	--color-vscode-list-hoverBackground: var(--vscode-list-hoverBackground);
 	--color-vscode-list-focusBackground: var(--vscode-list-focusBackground);
+
+	--color-vscode-toolbar-hoverBackground: var(--vscode-toolbar-hoverBackground);
+
+	--color-vscode-panel-border: var(--vscode-panel-border);
 }
 
 @layer base {