From 33f8e5883308c59a44ce8b67476489aa6fe1a551 Mon Sep 17 00:00:00 2001 From: Hannes Rudolph Date: Wed, 17 Dec 2025 20:52:41 -0700 Subject: [PATCH] fix: prevent stale cache when editing prompts across multiple windows Add refreshGlobalStateKey() method to ContextProxy that reads fresh values from VS Code's globalState before performing read-modify-write operations. This fixes a bug where editing the context condensing prompt or mode prompts would revert to a previous value when multiple VS Code windows are open, because each window had its own stale cache. --- src/core/config/ContextProxy.ts | 13 +++++ .../config/__tests__/ContextProxy.spec.ts | 51 +++++++++++++++++++ src/core/webview/webviewMessageHandler.ts | 8 ++- 3 files changed, 70 insertions(+), 2 deletions(-) diff --git a/src/core/config/ContextProxy.ts b/src/core/config/ContextProxy.ts index 64baf546bd..64adcc3b3d 100644 --- a/src/core/config/ContextProxy.ts +++ b/src/core/config/ContextProxy.ts @@ -208,6 +208,19 @@ export class ContextProxy { return this.originalContext.globalState.update(key, value) } + /** + * Refresh a specific key from globalState and update the cache. + * This is useful for settings that may be edited across multiple VS Code windows, + * ensuring we read the latest value before making modifications. + * @param key The global state key to refresh + * @returns The fresh value from globalState + */ + refreshGlobalStateKey(key: K): GlobalState[K] { + const value = this.originalContext.globalState.get(key) + this.stateCache[key] = value + return value as GlobalState[K] + } + private getAllGlobalState(): GlobalState { return Object.fromEntries(GLOBAL_STATE_KEYS.map((key) => [key, this.getGlobalState(key)])) } diff --git a/src/core/config/__tests__/ContextProxy.spec.ts b/src/core/config/__tests__/ContextProxy.spec.ts index 49e706b181..44f41bbf50 100644 --- a/src/core/config/__tests__/ContextProxy.spec.ts +++ b/src/core/config/__tests__/ContextProxy.spec.ts @@ -185,6 +185,57 @@ describe("ContextProxy", () => { }) }) + describe("refreshGlobalStateKey", () => { + it("should read fresh value from globalState and update cache", async () => { + // Set up initial cache value + await proxy.updateGlobalState("customSupportPrompts", { OLD: "old-prompt" }) + + // Simulate another window updating globalState directly (bypassing our cache) + const newValue = { OLD: "old-prompt", NEW: "new-prompt" } + mockGlobalState.get.mockReturnValue(newValue) + + // Refresh the key + const result = proxy.refreshGlobalStateKey("customSupportPrompts") + + // Should return the fresh value from globalState + expect(result).toEqual(newValue) + + // Should have updated the cache (subsequent getGlobalState should return new value) + const cachedValue = proxy.getGlobalState("customSupportPrompts") + expect(cachedValue).toEqual(newValue) + }) + + it("should return undefined when globalState has no value", async () => { + // Set up initial cache value + await proxy.updateGlobalState("customModePrompts", { code: { roleDefinition: "test" } }) + + // Simulate globalState being cleared (e.g., by another window) + mockGlobalState.get.mockReturnValue(undefined) + + // Refresh the key + const result = proxy.refreshGlobalStateKey("customModePrompts") + + // Should return undefined + expect(result).toBeUndefined() + + // Cache should also be updated to undefined + const cachedValue = proxy.getGlobalState("customModePrompts") + expect(cachedValue).toBeUndefined() + }) + + it("should call globalState.get with the correct key", async () => { + // Clear mock call history + mockGlobalState.get.mockClear() + mockGlobalState.get.mockReturnValue({ ENHANCE: "test-prompt" }) + + // Refresh a specific key + proxy.refreshGlobalStateKey("customSupportPrompts") + + // Should have called get with the correct key + expect(mockGlobalState.get).toHaveBeenCalledWith("customSupportPrompts") + }) + }) + describe("getSecret", () => { it("should return value from cache when it exists", async () => { // Manually set a value in the cache diff --git a/src/core/webview/webviewMessageHandler.ts b/src/core/webview/webviewMessageHandler.ts index e1640d3f2a..3ddc0b0472 100644 --- a/src/core/webview/webviewMessageHandler.ts +++ b/src/core/webview/webviewMessageHandler.ts @@ -1485,7 +1485,9 @@ export const webviewMessageHandler = async ( break case "updatePrompt": if (message.promptMode && message.customPrompt !== undefined) { - const existingPrompts = getGlobalState("customModePrompts") ?? {} + // Use refreshGlobalStateKey to get the latest value from globalState, + // avoiding stale cache issues when multiple VS Code windows are open. + const existingPrompts = provider.contextProxy.refreshGlobalStateKey("customModePrompts") ?? {} const updatedPrompts = { ...existingPrompts, [message.promptMode]: message.customPrompt } await updateGlobalState("customModePrompts", updatedPrompts) const currentState = await provider.getStateToPostToWebview() @@ -1571,7 +1573,9 @@ export const webviewMessageHandler = async ( case "updateCondensingPrompt": // Store the condensing prompt in customSupportPrompts["CONDENSE"] // instead of customCondensingPrompt. - const currentSupportPrompts = getGlobalState("customSupportPrompts") ?? {} + // Use refreshGlobalStateKey to get the latest value from globalState, + // avoiding stale cache issues when multiple VS Code windows are open. + const currentSupportPrompts = provider.contextProxy.refreshGlobalStateKey("customSupportPrompts") ?? {} const updatedSupportPrompts = { ...currentSupportPrompts, CONDENSE: message.text } await updateGlobalState("customSupportPrompts", updatedSupportPrompts) // Also update the old field for backward compatibility during migration.