Browse Source

Feat ContextProxy to improve state management

- Add ContextProxy class as a wrapper around VSCode's ExtensionContext
- Implement batched state updates for performance optimization
- Update ClineProvider to use ContextProxy instead of direct context access
- Add comprehensive test coverage for ContextProxy
- Extract SECRET_KEYS and GLOBAL_STATE_KEYS constants for better maintainability
sam hoang 10 tháng trước cách đây
mục cha
commit
381b07849a

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

@@ -0,0 +1,282 @@
+import * as vscode from "vscode"
+import { ContextProxy } from "../contextProxy"
+import { logger } from "../../utils/logging"
+
+// Mock the logger
+jest.mock("../../utils/logging", () => ({
+	logger: {
+		debug: jest.fn(),
+		info: jest.fn(),
+		warn: jest.fn(),
+		error: jest.fn(),
+	},
+}))
+
+// 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(),
+			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("getGlobalState", () => {
+		it("should return pending change when it exists", async () => {
+			// Set up a pending change
+			await proxy.updateGlobalState("test-key", "new-value")
+
+			// Should return the pending value
+			const result = await proxy.getGlobalState("test-key")
+			expect(result).toBe("new-value")
+
+			// Original context should not be called
+			expect(mockGlobalState.get).not.toHaveBeenCalled()
+		})
+
+		it("should fall back to original context when no pending change exists", async () => {
+			// Set up original context value
+			mockGlobalState.get.mockReturnValue("original-value")
+
+			// Should get from original context
+			const result = await proxy.getGlobalState("test-key")
+			expect(result).toBe("original-value")
+			expect(mockGlobalState.get).toHaveBeenCalledWith("test-key", undefined)
+		})
+
+		it("should handle default values correctly", async () => {
+			// No value in either pending or original
+			mockGlobalState.get.mockImplementation((key: string, defaultValue: any) => defaultValue)
+
+			// Should return the default value
+			const result = await proxy.getGlobalState("test-key", "default-value")
+			expect(result).toBe("default-value")
+		})
+	})
+
+	describe("updateGlobalState", () => {
+		it("should buffer changes without calling original context", async () => {
+			await proxy.updateGlobalState("test-key", "new-value")
+
+			// Should have called logger.debug
+			expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering state update"))
+
+			// Should not have called original context
+			expect(mockGlobalState.update).not.toHaveBeenCalled()
+
+			// Should have stored the value in pendingStateChanges
+			const storedValue = await proxy.getGlobalState("test-key")
+			expect(storedValue).toBe("new-value")
+		})
+
+		it("should throw an error when context is disposed", async () => {
+			await proxy.dispose()
+
+			await expect(proxy.updateGlobalState("test-key", "new-value")).rejects.toThrow(
+				"Cannot update state on disposed context",
+			)
+		})
+	})
+
+	describe("getSecret", () => {
+		it("should return pending secret when it exists", async () => {
+			// Set up a pending secret
+			await proxy.storeSecret("api-key", "secret123")
+
+			// Should return the pending value
+			const result = await proxy.getSecret("api-key")
+			expect(result).toBe("secret123")
+
+			// Original context should not be called
+			expect(mockSecrets.get).not.toHaveBeenCalled()
+		})
+
+		it("should fall back to original context when no pending secret exists", async () => {
+			// Set up original context value
+			mockSecrets.get.mockResolvedValue("original-secret")
+
+			// Should get from original context
+			const result = await proxy.getSecret("api-key")
+			expect(result).toBe("original-secret")
+			expect(mockSecrets.get).toHaveBeenCalledWith("api-key")
+		})
+	})
+
+	describe("storeSecret", () => {
+		it("should buffer secret changes without calling original context", async () => {
+			await proxy.storeSecret("api-key", "new-secret")
+
+			// Should have called logger.debug
+			expect(logger.debug).toHaveBeenCalledWith(expect.stringContaining("buffering secret update"))
+
+			// Should not have called original context
+			expect(mockSecrets.store).not.toHaveBeenCalled()
+
+			// Should have stored the value in pendingSecretChanges
+			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 stored undefined in pendingSecretChanges
+			const storedValue = await proxy.getSecret("api-key")
+			expect(storedValue).toBeUndefined()
+		})
+
+		it("should throw an error when context is disposed", async () => {
+			await proxy.dispose()
+
+			await expect(proxy.storeSecret("api-key", "new-secret")).rejects.toThrow(
+				"Cannot store secret on disposed context",
+			)
+		})
+	})
+
+	describe("saveChanges", () => {
+		it("should apply state changes to original context", async () => {
+			// Set up pending changes
+			await proxy.updateGlobalState("key1", "value1")
+			await proxy.updateGlobalState("key2", "value2")
+
+			// Save changes
+			await proxy.saveChanges()
+
+			// Should have called update on original context
+			expect(mockGlobalState.update).toHaveBeenCalledTimes(2)
+			expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
+			expect(mockGlobalState.update).toHaveBeenCalledWith("key2", "value2")
+
+			// Should have cleared pending changes
+			expect(proxy.hasPendingChanges()).toBe(false)
+		})
+
+		it("should apply secret changes to original context", async () => {
+			// Set up pending changes
+			await proxy.storeSecret("secret1", "value1")
+			await proxy.storeSecret("secret2", undefined)
+
+			// Save changes
+			await proxy.saveChanges()
+
+			// Should have called store and delete on original context
+			expect(mockSecrets.store).toHaveBeenCalledTimes(1)
+			expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
+			expect(mockSecrets.delete).toHaveBeenCalledTimes(1)
+			expect(mockSecrets.delete).toHaveBeenCalledWith("secret2")
+
+			// Should have cleared pending changes
+			expect(proxy.hasPendingChanges()).toBe(false)
+		})
+
+		it("should do nothing when there are no pending changes", async () => {
+			await proxy.saveChanges()
+
+			expect(mockGlobalState.update).not.toHaveBeenCalled()
+			expect(mockSecrets.store).not.toHaveBeenCalled()
+			expect(mockSecrets.delete).not.toHaveBeenCalled()
+		})
+
+		it("should throw an error when context is disposed", async () => {
+			await proxy.dispose()
+
+			await expect(proxy.saveChanges()).rejects.toThrow("Cannot save changes on disposed context")
+		})
+	})
+
+	describe("dispose", () => {
+		it("should save pending changes to original context", async () => {
+			// Set up pending changes
+			await proxy.updateGlobalState("key1", "value1")
+			await proxy.storeSecret("secret1", "value1")
+
+			// Dispose
+			await proxy.dispose()
+
+			// Should have saved changes
+			expect(mockGlobalState.update).toHaveBeenCalledWith("key1", "value1")
+			expect(mockSecrets.store).toHaveBeenCalledWith("secret1", "value1")
+
+			// Should be marked as disposed
+			expect(proxy.hasPendingChanges()).toBe(false)
+		})
+	})
+
+	describe("hasPendingChanges", () => {
+		it("should return false when no changes are pending", () => {
+			expect(proxy.hasPendingChanges()).toBe(false)
+		})
+
+		it("should return true when state changes are pending", async () => {
+			await proxy.updateGlobalState("key", "value")
+			expect(proxy.hasPendingChanges()).toBe(true)
+		})
+
+		it("should return true when secret changes are pending", async () => {
+			await proxy.storeSecret("key", "value")
+			expect(proxy.hasPendingChanges()).toBe(true)
+		})
+
+		it("should return false after changes are saved", async () => {
+			await proxy.updateGlobalState("key", "value")
+			expect(proxy.hasPendingChanges()).toBe(true)
+
+			await proxy.saveChanges()
+			expect(proxy.hasPendingChanges()).toBe(false)
+		})
+	})
+})

+ 123 - 0
src/core/contextProxy.ts

@@ -0,0 +1,123 @@
+import * as vscode from "vscode"
+import { logger } from "../utils/logging"
+
+/**
+ * A proxy class for vscode.ExtensionContext that buffers state changes
+ * and only commits them when explicitly requested or during disposal.
+ */
+export class ContextProxy {
+	private readonly originalContext: vscode.ExtensionContext
+	private pendingStateChanges: Map<string, any>
+	private pendingSecretChanges: Map<string, string | undefined>
+	private disposed: boolean
+
+	constructor(context: vscode.ExtensionContext) {
+		this.originalContext = context
+		this.pendingStateChanges = new Map()
+		this.pendingSecretChanges = new Map()
+		this.disposed = false
+		logger.debug("ContextProxy created")
+	}
+
+	// Read-only pass-through properties
+	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
+	}
+
+	// State management methods
+	async getGlobalState<T>(key: string): Promise<T | undefined>
+	async getGlobalState<T>(key: string, defaultValue: T): Promise<T>
+	async getGlobalState<T>(key: string, defaultValue?: T): Promise<T | undefined> {
+		// Check pending changes first
+		if (this.pendingStateChanges.has(key)) {
+			const value = this.pendingStateChanges.get(key) as T | undefined
+			return value !== undefined ? value : (defaultValue as T | undefined)
+		}
+		// Fall back to original context
+		return this.originalContext.globalState.get<T>(key, defaultValue as T)
+	}
+
+	async updateGlobalState<T>(key: string, value: T): Promise<void> {
+		if (this.disposed) {
+			throw new Error("Cannot update state on disposed context")
+		}
+		logger.debug(`ContextProxy: buffering state update for key "${key}"`)
+		this.pendingStateChanges.set(key, value)
+	}
+
+	// Secret storage methods
+	async getSecret(key: string): Promise<string | undefined> {
+		// Check pending changes first
+		if (this.pendingSecretChanges.has(key)) {
+			return this.pendingSecretChanges.get(key)
+		}
+		// Fall back to original context
+		return this.originalContext.secrets.get(key)
+	}
+
+	async storeSecret(key: string, value?: string): Promise<void> {
+		if (this.disposed) {
+			throw new Error("Cannot store secret on disposed context")
+		}
+		logger.debug(`ContextProxy: buffering secret update for key "${key}"`)
+		this.pendingSecretChanges.set(key, value)
+	}
+
+	// Save pending changes to actual context
+	async saveChanges(): Promise<void> {
+		if (this.disposed) {
+			throw new Error("Cannot save changes on disposed context")
+		}
+
+		// Apply state changes
+		if (this.pendingStateChanges.size > 0) {
+			logger.debug(`ContextProxy: applying ${this.pendingStateChanges.size} buffered state changes`)
+			for (const [key, value] of this.pendingStateChanges.entries()) {
+				await this.originalContext.globalState.update(key, value)
+			}
+			this.pendingStateChanges.clear()
+		}
+
+		// Apply secret changes
+		if (this.pendingSecretChanges.size > 0) {
+			logger.debug(`ContextProxy: applying ${this.pendingSecretChanges.size} buffered secret changes`)
+			for (const [key, value] of this.pendingSecretChanges.entries()) {
+				if (value === undefined) {
+					await this.originalContext.secrets.delete(key)
+				} else {
+					await this.originalContext.secrets.store(key, value)
+				}
+			}
+			this.pendingSecretChanges.clear()
+		}
+	}
+
+	// Called when the provider is disposing
+	async dispose(): Promise<void> {
+		if (!this.disposed) {
+			logger.debug("ContextProxy: disposing and saving pending changes")
+			await this.saveChanges()
+			this.disposed = true
+		}
+	}
+
+	// Method to check if there are pending changes
+	hasPendingChanges(): boolean {
+		return this.pendingStateChanges.size > 0 || this.pendingSecretChanges.size > 0
+	}
+}

+ 172 - 405
src/core/webview/ClineProvider.ts

@@ -12,7 +12,7 @@ import { ApiConfiguration, ApiProvider, ModelInfo } from "../../shared/api"
 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"
@@ -34,6 +34,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"
@@ -65,6 +66,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
 
@@ -73,6 +75,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)
@@ -115,6 +118,9 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		this.mcpHub = undefined
 		this.customModesManager?.dispose()
 		this.outputChannel.appendLine("Disposed all disposables")
+		// Dispose the context proxy to commit any pending changes
+		await this.contextProxy.dispose()
+		this.outputChannel.appendLine("Disposed context proxy")
 		ClineProvider.activeInstances.delete(this)
 
 		// Unregister from McpServerManager
@@ -241,11 +247,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)
 
@@ -389,8 +395,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",
@@ -456,15 +467,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",
@@ -1249,7 +1265,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) {
@@ -1628,108 +1646,21 @@ 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,
-			lmStudioDraftModelId,
-			lmStudioSpeculativeDecodingEnabled,
-		} = 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),
-			this.updateGlobalState("lmStudioDraftModelId", lmStudioDraftModelId),
-			this.updateGlobalState("lmStudioSpeculativeDecodingEnabled", lmStudioSpeculativeDecodingEnabled),
-		])
+		// Create an array of promises to update state
+		const promises: Promise<any>[] = []
+
+		// For each property in apiConfiguration, update the appropriate state
+		Object.entries(apiConfiguration).forEach(([key, value]) => {
+			// Check if this key is a secret
+			if (SECRET_KEYS.includes(key as SecretKey)) {
+				promises.push(this.storeSecret(key as SecretKey, value))
+			} else {
+				promises.push(this.updateGlobalState(key as GlobalStateKey, value))
+			}
+		})
+
+		await Promise.all(promises)
+
 		if (this.cline) {
 			this.cline.api = buildApiHandler(apiConfiguration)
 		}
@@ -1790,13 +1721,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
 	}
@@ -1884,7 +1815,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)
@@ -2049,7 +1980,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 || [])
@@ -2140,189 +2071,41 @@ 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,
-			lmStudioSpeculativeDecodingEnabled,
-			lmStudioDraftModelId,
-		] = 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>,
-			this.getGlobalState("lmStudioSpeculativeDecodingEnabled") as Promise<boolean | undefined>,
-			this.getGlobalState("lmStudioDraftModelId") as Promise<string | undefined>,
+		// Create an object to store all fetched values
+		const stateValues: Record<GlobalStateKey | SecretKey, any> = {} as Record<GlobalStateKey | SecretKey, any>
+		const secretValues: Record<SecretKey, any> = {} as Record<SecretKey, any>
+
+		// Create promise arrays for global state and secrets
+		const statePromises = GLOBAL_STATE_KEYS.map((key) => this.getGlobalState(key))
+		const secretPromises = SECRET_KEYS.map((key) => this.getSecret(key))
+
+		// Add promise for custom modes which is handled separately
+		const customModesPromise = this.customModesManager.getCustomModes()
+
+		// Wait for all promises to resolve
+		const [stateResults, secretResults, customModes] = await Promise.all([
+			Promise.all(statePromises),
+			Promise.all(secretPromises),
+			customModesPromise,
 		])
 
+		// Populate stateValues and secretValues
+		GLOBAL_STATE_KEYS.forEach((key, index) => {
+			stateValues[key] = stateResults[index]
+		})
+
+		SECRET_KEYS.forEach((key, index) => {
+			secretValues[key] = secretResults[index]
+		})
+
+		// Determine apiProvider with the same logic as before
 		let apiProvider: ApiProvider
-		if (storedApiProvider) {
-			apiProvider = storedApiProvider
+		if (stateValues.apiProvider) {
+			apiProvider = stateValues.apiProvider
 		} 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) {
+			if (secretValues.apiKey) {
 				apiProvider = "anthropic"
 			} else {
 				// New users should default to openrouter
@@ -2330,80 +2113,73 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 			}
 		}
 
+		// Build the apiConfiguration object combining state values and secrets
+		const apiConfiguration: ApiConfiguration = {
+			apiProvider,
+			apiModelId: stateValues.apiModelId,
+			glamaModelId: stateValues.glamaModelId,
+			glamaModelInfo: stateValues.glamaModelInfo,
+			awsRegion: stateValues.awsRegion,
+			awsUseCrossRegionInference: stateValues.awsUseCrossRegionInference,
+			awsProfile: stateValues.awsProfile,
+			awsUseProfile: stateValues.awsUseProfile,
+			vertexProjectId: stateValues.vertexProjectId,
+			vertexRegion: stateValues.vertexRegion,
+			openAiBaseUrl: stateValues.openAiBaseUrl,
+			openAiModelId: stateValues.openAiModelId,
+			openAiCustomModelInfo: stateValues.openAiCustomModelInfo,
+			openAiUseAzure: stateValues.openAiUseAzure,
+			ollamaModelId: stateValues.ollamaModelId,
+			ollamaBaseUrl: stateValues.ollamaBaseUrl,
+			lmStudioModelId: stateValues.lmStudioModelId,
+			lmStudioBaseUrl: stateValues.lmStudioBaseUrl,
+			anthropicBaseUrl: stateValues.anthropicBaseUrl,
+			modelMaxThinkingTokens: stateValues.modelMaxThinkingTokens,
+			mistralCodestralUrl: stateValues.mistralCodestralUrl,
+			azureApiVersion: stateValues.azureApiVersion,
+			openAiStreamingEnabled: stateValues.openAiStreamingEnabled,
+			openRouterModelId: stateValues.openRouterModelId,
+			openRouterModelInfo: stateValues.openRouterModelInfo,
+			openRouterBaseUrl: stateValues.openRouterBaseUrl,
+			openRouterUseMiddleOutTransform: stateValues.openRouterUseMiddleOutTransform,
+			vsCodeLmModelSelector: stateValues.vsCodeLmModelSelector,
+			unboundModelId: stateValues.unboundModelId,
+			unboundModelInfo: stateValues.unboundModelInfo,
+			requestyModelId: stateValues.requestyModelId,
+			requestyModelInfo: stateValues.requestyModelInfo,
+			modelTemperature: stateValues.modelTemperature,
+			modelMaxTokens: stateValues.modelMaxTokens,
+			lmStudioSpeculativeDecodingEnabled: stateValues.lmStudioSpeculativeDecodingEnabled,
+			lmStudioDraftModelId: stateValues.lmStudioDraftModelId,
+			// Add all secrets
+			...secretValues,
+		}
+
+		// 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,
-				lmStudioSpeculativeDecodingEnabled,
-				lmStudioDraftModelId,
-			},
-			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,
+			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
@@ -2433,23 +2209,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,
 		}
 	}
 
@@ -2469,25 +2245,29 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 	// global
 
 	async updateGlobalState(key: GlobalStateKey, value: any) {
-		await this.context.globalState.update(key, value)
+		this.outputChannel.appendLine(`Updating global state: ${key}`)
+		await this.contextProxy.updateGlobalState(key, value)
+
+		// // If we have a lot of pending changes, consider saving them periodically
+		// if (this.contextProxy.hasPendingChanges() && Math.random() < 0.1) { // 10% chance to save changes
+		// 	this.outputChannel.appendLine("Periodically flushing context state changes")
+		// 	await this.contextProxy.saveChanges()
+		// }
 	}
 
 	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)
-		}
+		this.outputChannel.appendLine(`Storing secret: ${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
@@ -2504,24 +2284,11 @@ export class ClineProvider implements vscode.WebviewViewProvider {
 		}
 
 		for (const key of this.context.globalState.keys()) {
-			await this.context.globalState.update(key, undefined)
+			// Still using original context for listing keys
+			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()

+ 130 - 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,34 @@ 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),
+				),
+			saveChanges: jest.fn().mockResolvedValue(undefined),
+			dispose: jest.fn().mockResolvedValue(undefined),
+			hasPendingChanges: jest.fn().mockReturnValue(false),
+		})),
+	}
+})
+
 // Mock dependencies
 jest.mock("vscode")
 jest.mock("delay")
@@ -153,6 +182,16 @@ jest.mock("../../../utils/sound", () => ({
 	setSoundEnabled: jest.fn(),
 }))
 
+// Mock logger
+jest.mock("../../../utils/logging", () => ({
+	logger: {
+		debug: jest.fn(),
+		error: jest.fn(),
+		warn: jest.fn(),
+		info: jest.fn(),
+	},
+}))
+
 // Mock ESM modules
 jest.mock("p-wait-for", () => ({
 	__esModule: true,
@@ -235,6 +274,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 +352,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
@@ -478,6 +525,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 +539,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 +662,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()
 
@@ -1253,6 +1303,17 @@ describe("ClineProvider", () => {
 			// Verify state was posted to webview
 			expect(mockPostMessage).toHaveBeenCalledWith(expect.objectContaining({ type: "state" }))
 		})
+
+		test("disposes the contextProxy when provider is disposed", async () => {
+			// Setup mock Cline instance
+			const mockCline = {
+				abortTask: jest.fn(),
+			}
+			// @ts-ignore - accessing private property for testing
+			provider.cline = mockCline
+			await provider.dispose()
+			expect(mockContextProxy.dispose).toHaveBeenCalled()
+		})
 	})
 
 	describe("updateCustomMode", () => {
@@ -1474,6 +1535,7 @@ describe("ClineProvider", () => {
 				apiConfiguration: testApiConfig,
 			})
 
+			// Reset jest.mock calls tracking
 			// Verify config was saved
 			expect(provider.configManager.saveConfig).toHaveBeenCalledWith("test-config", testApiConfig)
 
@@ -1481,6 +1543,74 @@ 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" },
+			])
+
+			// Reset jest.mock calls tracking for subsequent tests
+			jest.clearAllMocks()
 		})
 	})
 })
+
+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()
+	})
+
+	test("contextProxy is properly disposed", async () => {
+		// Setup mock Cline instance
+		const mockCline = {
+			abortTask: jest.fn(),
+		}
+		// @ts-ignore - accessing private property for testing
+		provider.cline = mockCline
+		await provider.dispose()
+		expect(mockContextProxy.dispose).toHaveBeenCalled()
+	})
+})

+ 90 - 1
src/shared/globalState.ts

@@ -13,6 +13,22 @@ export type SecretKey =
 	| "unboundApiKey"
 	| "requestyApiKey"
 
+export const SECRET_KEYS: SecretKey[] = [
+	"apiKey",
+	"glamaApiKey",
+	"openRouterApiKey",
+	"awsAccessKey",
+	"awsSecretKey",
+	"awsSessionToken",
+	"openAiApiKey",
+	"geminiApiKey",
+	"openAiNativeApiKey",
+	"deepSeekApiKey",
+	"mistralApiKey",
+	"unboundApiKey",
+	"requestyApiKey",
+]
+
 export type GlobalStateKey =
 	| "apiProvider"
 	| "apiModelId"
@@ -83,7 +99,80 @@ export type GlobalStateKey =
 	| "unboundModelInfo"
 	| "modelTemperature"
 	| "modelMaxTokens"
-	| "anthropicThinking" // TODO: Rename to `modelMaxThinkingTokens`.
+	| "modelMaxThinkingTokens"
 	| "mistralCodestralUrl"
 	| "maxOpenTabsContext"
 	| "browserToolEnabled" // Setting to enable/disable the browser tool
+
+export const GLOBAL_STATE_KEYS: 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",
+	"modelMaxThinkingTokens",
+	"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",
+	"mistralCodestralUrl",
+	"maxOpenTabsContext",
+]