mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-08-28 05:27:24 +00:00
fix: isolate state between duplicate VSCode workspaces using sessionId
- Add session-specific keys to ContextProxy to prevent state sharing - Use vscode.env.sessionId to create unique state keys per window - Maintain shared keys for API configurations that should persist - Handle cases where sessionId is not available (e.g., in tests) Fixes #7698
This commit is contained in:
parent
90e7d09596
commit
02e8716cb6
2 changed files with 85 additions and 25 deletions
|
|
@ -36,6 +36,7 @@ const globalSettingsExportSchema = globalSettingsSchema.omit({
|
|||
|
||||
export class ContextProxy {
|
||||
private readonly originalContext: vscode.ExtensionContext
|
||||
private readonly sessionId: string
|
||||
|
||||
private stateCache: GlobalState
|
||||
private secretCache: SecretState
|
||||
|
|
@ -43,6 +44,10 @@ export class ContextProxy {
|
|||
|
||||
constructor(context: vscode.ExtensionContext) {
|
||||
this.originalContext = context
|
||||
// Use sessionId to isolate state between multiple VSCode windows
|
||||
// This ensures each window maintains its own independent state
|
||||
// Fallback to empty string if sessionId is not available (e.g., in tests)
|
||||
this.sessionId = vscode.env.sessionId || ""
|
||||
this.stateCache = {}
|
||||
this.secretCache = {}
|
||||
this._isInitialized = false
|
||||
|
|
@ -55,8 +60,9 @@ export class ContextProxy {
|
|||
public async initialize() {
|
||||
for (const key of GLOBAL_STATE_KEYS) {
|
||||
try {
|
||||
// Revert to original assignment
|
||||
this.stateCache[key] = this.originalContext.globalState.get(key)
|
||||
// Use session-specific key for state isolation
|
||||
const sessionKey = this.getSessionKey(key)
|
||||
this.stateCache[key] = this.originalContext.globalState.get(sessionKey)
|
||||
} catch (error) {
|
||||
logger.error(`Error loading global ${key}: ${error instanceof Error ? error.message : String(error)}`)
|
||||
}
|
||||
|
|
@ -91,13 +97,38 @@ export class ContextProxy {
|
|||
this._isInitialized = true
|
||||
}
|
||||
|
||||
/**
|
||||
* Creates a session-specific key by combining the base key with the session ID.
|
||||
* This ensures state isolation between multiple VSCode windows.
|
||||
*
|
||||
* @param key The base state key
|
||||
* @returns The session-specific key
|
||||
*/
|
||||
private getSessionKey(key: string): string {
|
||||
// For certain keys that should be shared across sessions (like API configs),
|
||||
// we don't add the session prefix
|
||||
const sharedKeys = ["listApiConfigMeta", "currentApiConfigName", "apiProvider"]
|
||||
if (sharedKeys.includes(key)) {
|
||||
return key
|
||||
}
|
||||
|
||||
// If no sessionId is available (e.g., in tests), use the key as-is
|
||||
if (!this.sessionId) {
|
||||
return key
|
||||
}
|
||||
|
||||
// For all other keys, add session prefix to isolate state
|
||||
return `session_${this.sessionId}_${key}`
|
||||
}
|
||||
|
||||
/**
|
||||
* Migrates old nested openRouterImageGenerationSettings to the new flattened structure
|
||||
*/
|
||||
private async migrateImageGenerationSettings() {
|
||||
try {
|
||||
// Check if there's an old nested structure
|
||||
const oldNestedSettings = this.originalContext.globalState.get<any>("openRouterImageGenerationSettings")
|
||||
// Check if there's an old nested structure (use session-specific key)
|
||||
const sessionKey = this.getSessionKey("openRouterImageGenerationSettings")
|
||||
const oldNestedSettings = this.originalContext.globalState.get<any>(sessionKey)
|
||||
|
||||
if (oldNestedSettings && typeof oldNestedSettings === "object") {
|
||||
logger.info("Migrating old nested image generation settings to flattened structure")
|
||||
|
|
@ -114,16 +145,14 @@ export class ContextProxy {
|
|||
|
||||
// Migrate the selected model if it exists and we don't already have one
|
||||
if (oldNestedSettings.selectedModel && !this.stateCache.openRouterImageGenerationSelectedModel) {
|
||||
await this.originalContext.globalState.update(
|
||||
"openRouterImageGenerationSelectedModel",
|
||||
oldNestedSettings.selectedModel,
|
||||
)
|
||||
const modelSessionKey = this.getSessionKey("openRouterImageGenerationSelectedModel")
|
||||
await this.originalContext.globalState.update(modelSessionKey, oldNestedSettings.selectedModel)
|
||||
this.stateCache.openRouterImageGenerationSelectedModel = oldNestedSettings.selectedModel
|
||||
logger.info("Migrated openRouterImageGenerationSelectedModel to global state")
|
||||
}
|
||||
|
||||
// Clean up the old nested structure
|
||||
await this.originalContext.globalState.update("openRouterImageGenerationSettings", undefined)
|
||||
await this.originalContext.globalState.update(sessionKey, undefined)
|
||||
logger.info("Removed old nested openRouterImageGenerationSettings")
|
||||
}
|
||||
} catch (error) {
|
||||
|
|
@ -166,7 +195,9 @@ export class ContextProxy {
|
|||
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue: GlobalState[K]): GlobalState[K]
|
||||
getGlobalState<K extends GlobalStateKey>(key: K, defaultValue?: GlobalState[K]): GlobalState[K] {
|
||||
if (isPassThroughStateKey(key)) {
|
||||
const value = this.originalContext.globalState.get<GlobalState[K]>(key)
|
||||
// Use session-specific key for pass-through state as well
|
||||
const sessionKey = this.getSessionKey(key)
|
||||
const value = this.originalContext.globalState.get<GlobalState[K]>(sessionKey)
|
||||
return value === undefined || value === null ? defaultValue : value
|
||||
}
|
||||
|
||||
|
|
@ -175,12 +206,14 @@ export class ContextProxy {
|
|||
}
|
||||
|
||||
updateGlobalState<K extends GlobalStateKey>(key: K, value: GlobalState[K]) {
|
||||
const sessionKey = this.getSessionKey(key)
|
||||
|
||||
if (isPassThroughStateKey(key)) {
|
||||
return this.originalContext.globalState.update(key, value)
|
||||
return this.originalContext.globalState.update(sessionKey, value)
|
||||
}
|
||||
|
||||
this.stateCache[key] = value
|
||||
return this.originalContext.globalState.update(key, value)
|
||||
return this.originalContext.globalState.update(sessionKey, value)
|
||||
}
|
||||
|
||||
private getAllGlobalState(): GlobalState {
|
||||
|
|
@ -362,7 +395,10 @@ export class ContextProxy {
|
|||
this.secretCache = {}
|
||||
|
||||
await Promise.all([
|
||||
...GLOBAL_STATE_KEYS.map((key) => this.originalContext.globalState.update(key, undefined)),
|
||||
...GLOBAL_STATE_KEYS.map((key) => {
|
||||
const sessionKey = this.getSessionKey(key)
|
||||
return this.originalContext.globalState.update(sessionKey, undefined)
|
||||
}),
|
||||
...SECRET_STATE_KEYS.map((key) => this.originalContext.secrets.delete(key)),
|
||||
...GLOBAL_SECRET_KEYS.map((key) => this.originalContext.secrets.delete(key)),
|
||||
])
|
||||
|
|
|
|||
|
|
@ -15,6 +15,9 @@ vi.mock("vscode", () => ({
|
|||
Production: 2,
|
||||
Test: 3,
|
||||
},
|
||||
env: {
|
||||
sessionId: "test-session-id-12345",
|
||||
},
|
||||
}))
|
||||
|
||||
describe("ContextProxy", () => {
|
||||
|
|
@ -72,11 +75,21 @@ describe("ContextProxy", () => {
|
|||
it("should initialize state cache with all global state keys", () => {
|
||||
// +1 for the migration check of old nested settings
|
||||
expect(mockGlobalState.get).toHaveBeenCalledTimes(GLOBAL_STATE_KEYS.length + 1)
|
||||
|
||||
// When sessionId is available, session-specific keys are used for non-shared keys
|
||||
// In test environment with mocked sessionId, check for session-specific keys
|
||||
const sharedKeys = ["listApiConfigMeta", "currentApiConfigName", "apiProvider"]
|
||||
for (const key of GLOBAL_STATE_KEYS) {
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith(key)
|
||||
if (sharedKeys.includes(key)) {
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith(key)
|
||||
} else {
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith(`session_test-session-id-12345_${key}`)
|
||||
}
|
||||
}
|
||||
// Also check for migration call
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith("openRouterImageGenerationSettings")
|
||||
// Also check for migration call with session-specific key
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith(
|
||||
"session_test-session-id-12345_openRouterImageGenerationSettings",
|
||||
)
|
||||
})
|
||||
|
||||
it("should initialize secret cache with all secret keys", () => {
|
||||
|
|
@ -116,9 +129,9 @@ describe("ContextProxy", () => {
|
|||
// Use a pass-through key (taskHistory)
|
||||
const result = proxy.getGlobalState("taskHistory")
|
||||
|
||||
// Should get value directly from original context
|
||||
// Should get value directly from original context with session-specific key (when sessionId is available)
|
||||
expect(result).toBe("pass-through-value")
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith("session_test-session-id-12345_taskHistory")
|
||||
})
|
||||
|
||||
it("should respect default values for pass-through state keys", async () => {
|
||||
|
|
@ -149,7 +162,7 @@ describe("ContextProxy", () => {
|
|||
it("should update state directly in original context", async () => {
|
||||
await proxy.updateGlobalState("apiProvider", "deepseek")
|
||||
|
||||
// Should have called original context
|
||||
// Should have called original context (apiProvider is a shared key, no session prefix)
|
||||
expect(mockGlobalState.update).toHaveBeenCalledWith("apiProvider", "deepseek")
|
||||
|
||||
// Should have stored the value in cache
|
||||
|
|
@ -172,16 +185,19 @@ describe("ContextProxy", () => {
|
|||
|
||||
await proxy.updateGlobalState("taskHistory", historyItems)
|
||||
|
||||
// Should update original context
|
||||
expect(mockGlobalState.update).toHaveBeenCalledWith("taskHistory", historyItems)
|
||||
// Should update original context with session-specific key (when sessionId is available)
|
||||
expect(mockGlobalState.update).toHaveBeenCalledWith(
|
||||
"session_test-session-id-12345_taskHistory",
|
||||
historyItems,
|
||||
)
|
||||
|
||||
// Setup mock for subsequent get
|
||||
mockGlobalState.get.mockReturnValue(historyItems)
|
||||
|
||||
// Should get fresh value from original context
|
||||
// Should get fresh value from original context with session-specific key (when sessionId is available)
|
||||
const storedValue = proxy.getGlobalState("taskHistory")
|
||||
expect(storedValue).toBe(historyItems)
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith("taskHistory")
|
||||
expect(mockGlobalState.get).toHaveBeenCalledWith("session_test-session-id-12345_taskHistory")
|
||||
})
|
||||
})
|
||||
|
||||
|
|
@ -387,9 +403,17 @@ describe("ContextProxy", () => {
|
|||
// Reset all state
|
||||
await proxy.resetAllState()
|
||||
|
||||
// Should have called update with undefined for each key
|
||||
// Should have called update with undefined for each key using session-specific keys (when sessionId is available)
|
||||
const sharedKeys = ["listApiConfigMeta", "currentApiConfigName", "apiProvider"]
|
||||
for (const key of GLOBAL_STATE_KEYS) {
|
||||
expect(mockGlobalState.update).toHaveBeenCalledWith(key, undefined)
|
||||
if (sharedKeys.includes(key)) {
|
||||
expect(mockGlobalState.update).toHaveBeenCalledWith(key, undefined)
|
||||
} else {
|
||||
expect(mockGlobalState.update).toHaveBeenCalledWith(
|
||||
`session_test-session-id-12345_${key}`,
|
||||
undefined,
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// Total calls should include initial setup + reset operations
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue