@@ -2,16 +2,26 @@ import * as vscode from "vscode"
22import { logger } from "../utils/logging"
33import { GLOBAL_STATE_KEYS , SECRET_KEYS } from "../shared/globalState"
44
5+ // Keys that should be stored per-window rather than globally
6+ export const WINDOW_SPECIFIC_KEYS = [ "mode" ] as const
7+ export type WindowSpecificKey = ( typeof WINDOW_SPECIFIC_KEYS ) [ number ]
8+
59export class ContextProxy {
610 private readonly originalContext : vscode . ExtensionContext
711 private stateCache : Map < string , any >
812 private secretCache : Map < string , string | undefined >
13+ private windowId : string
14+ private readonly instanceCreationTime : Date = new Date ( )
915
1016 constructor ( context : vscode . ExtensionContext ) {
1117 // Initialize properties first
1218 this . originalContext = context
1319 this . stateCache = new Map ( )
1420 this . secretCache = new Map ( )
21+
22+ // Generate a unique ID for this window instance
23+ this . windowId = this . ensureUniqueWindowId ( )
24+ logger . debug ( `ContextProxy created with windowId: ${ this . windowId } ` )
1525
1626 // Initialize state cache with all defined global state keys
1727 this . initializeStateCache ( )
@@ -22,12 +32,125 @@ export class ContextProxy {
2232 logger . debug ( "ContextProxy created" )
2333 }
2434
35+ /**
36+ * Ensures we have a unique window ID, with fallback mechanisms if primary generation fails
37+ * @returns A string ID unique to this VS Code window
38+ */
39+ private ensureUniqueWindowId ( ) : string {
40+ // Try to get a stable ID first
41+ let id = this . generateWindowId ( ) ;
42+
43+ // If all else fails, use a purely random ID as ultimate fallback
44+ // This will not be stable across restarts but ensures uniqueness
45+ if ( ! id ) {
46+ id = `random_${ Date . now ( ) } _${ Math . random ( ) . toString ( 36 ) . substring ( 2 , 9 ) } ` ;
47+ logger . warn ( "Failed to generate stable window ID, using random ID instead" ) ;
48+ }
49+
50+ return id ;
51+ }
52+
53+ /**
54+ * Generates a unique identifier for the current VS Code window
55+ * This is used to namespace certain global state values to prevent
56+ * conflicts when using multiple VS Code windows.
57+ *
58+ * The ID generation uses multiple sources to ensure uniqueness even in
59+ * environments where workspace folders might be identical (like DevContainers).
60+ *
61+ * @returns A string ID unique to this VS Code window
62+ */
63+ private generateWindowId ( ) : string {
64+ try {
65+ // Get all available identifying information
66+ const folders = vscode . workspace . workspaceFolders || [ ] ;
67+ const workspaceName = vscode . workspace . name || "unknown" ;
68+ const folderPaths = folders . map ( folder => folder . uri . toString ( ) ) . join ( '|' ) ;
69+
70+ // Generate a stable, pseudorandom ID based on the workspace information
71+ // This will be consistent for the same workspace but different across workspaces
72+ const baseId = `${ workspaceName } |${ folderPaths } ` ;
73+
74+ // Add machine-specific information (will differ between host and containers)
75+ // env.machineId is stable across VS Code sessions on the same machine
76+ const machineSpecificId = vscode . env . machineId || "" ;
77+
78+ // Add a session component that distinguishes multiple windows with the same workspace
79+ // Creates a stable but reasonably unique hash
80+ const sessionHash = this . createSessionHash ( baseId ) ;
81+
82+ // Combine all components
83+ return `${ baseId } |${ machineSpecificId } |${ sessionHash } ` ;
84+ } catch ( error ) {
85+ logger . error ( "Error generating window ID:" , error ) ;
86+ return "" ; // Empty string triggers the fallback in ensureUniqueWindowId
87+ }
88+ }
89+
90+ /**
91+ * Creates a stable hash from input string and window-specific properties
92+ * that will be different for different VS Code windows even with identical projects
93+ */
94+ private createSessionHash ( input : string ) : string {
95+ try {
96+ // Use a combination of:
97+ // 1. The extension instance creation time
98+ const timestamp = this . instanceCreationTime . getTime ( ) ;
99+
100+ // 2. VS Code window-specific info we can derive
101+ // Using vscode.env.sessionId which changes on each VS Code window startup
102+ const sessionInfo = vscode . env . sessionId || "" ;
103+
104+ // 3. Calculate a simple hash
105+ const hashStr = `${ input } |${ sessionInfo } |${ timestamp } ` ;
106+ let hash = 0 ;
107+ for ( let i = 0 ; i < hashStr . length ; i ++ ) {
108+ const char = hashStr . charCodeAt ( i ) ;
109+ hash = ( ( hash << 5 ) - hash ) + char ;
110+ hash = hash & hash ; // Convert to 32bit integer
111+ }
112+
113+ // Return a hexadecimal representation
114+ return Math . abs ( hash ) . toString ( 16 ) . substring ( 0 , 8 ) ;
115+ } catch ( error ) {
116+ logger . error ( "Error creating session hash:" , error ) ;
117+ return Math . random ( ) . toString ( 36 ) . substring ( 2 , 10 ) ; // Random fallback
118+ }
119+ }
120+
121+ /**
122+ * Checks if a key should be stored per-window
123+ * @param key The key to check
124+ * @returns True if the key should be stored per-window, false otherwise
125+ */
126+ private isWindowSpecificKey ( key : string ) : boolean {
127+ return WINDOW_SPECIFIC_KEYS . includes ( key as WindowSpecificKey )
128+ }
129+
130+ /**
131+ * Converts a regular key to a window-specific key
132+ * @param key The original key
133+ * @returns The window-specific key with window ID prefix
134+ */
135+ private getWindowSpecificKey ( key : string ) : string {
136+ return `window:${ this . windowId } :${ key } `
137+ }
138+
25139 // Helper method to initialize state cache
26140 private initializeStateCache ( ) : void {
27141 for ( const key of GLOBAL_STATE_KEYS ) {
28142 try {
29- const value = this . originalContext . globalState . get ( key )
30- this . stateCache . set ( key , value )
143+ if ( this . isWindowSpecificKey ( key ) ) {
144+ // For window-specific keys, get the value using the window-specific key
145+ const windowKey = this . getWindowSpecificKey ( key )
146+ const value = this . originalContext . globalState . get ( windowKey )
147+ this . stateCache . set ( key , value )
148+ logger . debug ( `Loaded window-specific key ${ key } as ${ windowKey } with value: ${ value } ` )
149+ } else {
150+ // For global keys, use the regular key
151+ const value = this . originalContext . globalState . get ( key )
152+ this . stateCache . set ( key , value )
153+ }
31154 } catch ( error ) {
32155 logger . error ( `Error loading global ${ key } : ${ error instanceof Error ? error . message : String ( error ) } ` )
33156 }
@@ -70,13 +193,30 @@ export class ContextProxy {
70193 getGlobalState < T > ( key : string ) : T | undefined
71194 getGlobalState < T > ( key : string , defaultValue : T ) : T
72195 getGlobalState < T > ( key : string , defaultValue ?: T ) : T | undefined {
73- const value = this . stateCache . get ( key ) as T | undefined
74- return value !== undefined ? value : ( defaultValue as T | undefined )
196+ // Check for window-specific key
197+ if ( this . isWindowSpecificKey ( key ) ) {
198+ // Use the cached value if it exists (it would have been loaded with the window-specific key)
199+ const value = this . stateCache . get ( key ) as T | undefined
200+ return value !== undefined ? value : ( defaultValue as T | undefined )
201+ } else {
202+ // For regular global keys, use the regular cached value
203+ const value = this . stateCache . get ( key ) as T | undefined
204+ return value !== undefined ? value : ( defaultValue as T | undefined )
205+ }
75206 }
76207
77208 updateGlobalState < T > ( key : string , value : T ) : Thenable < void > {
209+ // Update in-memory cache
78210 this . stateCache . set ( key , value )
79- return this . originalContext . globalState . update ( key , value )
211+
212+ // Update in VSCode storage with appropriate key
213+ if ( this . isWindowSpecificKey ( key ) ) {
214+ const windowKey = this . getWindowSpecificKey ( key )
215+ logger . debug ( `Updating window-specific key ${ key } as ${ windowKey } with value: ${ JSON . stringify ( value ) } ` )
216+ return this . originalContext . globalState . update ( windowKey , value )
217+ } else {
218+ return this . originalContext . globalState . update ( key , value )
219+ }
80220 }
81221
82222 getSecret ( key : string ) : string | undefined {
@@ -140,16 +280,27 @@ export class ContextProxy {
140280 this . stateCache . clear ( )
141281 this . secretCache . clear ( )
142282
283+ // Create an array for all reset promises
284+ const resetPromises : Thenable < void > [ ] = [ ]
285+
143286 // Reset all global state values to undefined
144- const stateResetPromises = GLOBAL_STATE_KEYS . map ( ( key ) =>
145- this . originalContext . globalState . update ( key , undefined ) ,
146- )
287+ for ( const key of GLOBAL_STATE_KEYS ) {
288+ if ( this . isWindowSpecificKey ( key ) ) {
289+ // For window-specific keys, reset using the window-specific key
290+ const windowKey = this . getWindowSpecificKey ( key )
291+ resetPromises . push ( this . originalContext . globalState . update ( windowKey , undefined ) )
292+ } else {
293+ resetPromises . push ( this . originalContext . globalState . update ( key , undefined ) )
294+ }
295+ }
147296
148297 // Delete all secrets
149- const secretResetPromises = SECRET_KEYS . map ( ( key ) => this . originalContext . secrets . delete ( key ) )
298+ for ( const key of SECRET_KEYS ) {
299+ resetPromises . push ( this . originalContext . secrets . delete ( key ) )
300+ }
150301
151302 // Wait for all reset operations to complete
152- await Promise . all ( [ ... stateResetPromises , ... secretResetPromises ] )
303+ await Promise . all ( resetPromises )
153304
154305 this . initializeStateCache ( )
155306 this . initializeSecretCache ( )
0 commit comments