From be7d845ec0d1f0d6b2c26b678d92864187a5b28c Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Thu, 30 Jul 2026 15:02:22 +0000 Subject: [PATCH 01/12] fix: persist workbench state on the remote Fixes #4212 --- .github/workflows/build.yaml | 2 +- patches/remote-secret-storage.diff | 86 ++ patches/remote-storage.diff | 1193 ++++++++++++++++++++++++++++ patches/series | 2 + 4 files changed, 1282 insertions(+), 1 deletion(-) create mode 100644 patches/remote-secret-storage.diff create mode 100644 patches/remote-storage.diff diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index f9cf0e7064dd..e9555df6d814 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -194,7 +194,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: lib/vscode-reh-web-linux-x64 - key: vscode-linux-x64-package-${{ secrets.VSCODE_CACHE_VERSION }}-${{ steps.vscode-rev.outputs.rev }}-${{ hashFiles('patches/*.diff', 'ci/build/build-vscode.sh') }} + key: vscode-linux-x64-package-${{ secrets.VSCODE_CACHE_VERSION }}-${{ steps.vscode-rev.outputs.rev }}-${{ hashFiles('patches/*.diff', 'patches/series', 'ci/build/build-vscode.sh') }} - name: Build vscode if: steps.cache-vscode.outputs.cache-hit != 'true' run: | diff --git a/patches/remote-secret-storage.diff b/patches/remote-secret-storage.diff new file mode 100644 index 000000000000..904d1fc6df4c --- /dev/null +++ b/patches/remote-secret-storage.diff @@ -0,0 +1,86 @@ +AI: store secrets on remote for VS Code Web +Index: code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/platform/secrets/common/secrets.ts ++++ code-server/lib/vscode/src/vs/platform/secrets/common/secrets.ts +@@ -139,8 +139,13 @@ export class BaseSecretStorageService ex + return await readEncryptedSecret( + key, + (fullKey) => this.getValueFromStorage(key, fullKey, storageService), +- // If the storage service is in-memory, we don't need to decrypt +- this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.decrypt(v), ++ // Don't decrypt if storage is in-memory or if encryption service is not available ++ async (v) => { ++ if (this._type === 'in-memory' || !await this._encryptionService.isEncryptionAvailable()) { ++ return v; ++ } ++ return this._encryptionService.decrypt(v); ++ }, + this._logService, + ); + } catch (e) { +@@ -160,8 +165,13 @@ export class BaseSecretStorageService ex + key, + value, + (fullKey, encrypted) => this.setValueInStorage(key, fullKey, encrypted, storageService), +- // If the storage service is in-memory, we don't need to encrypt +- this._type === 'in-memory' ? (v) => Promise.resolve(v) : (v) => this._encryptionService.encrypt(v), ++ // Don't encrypt if storage is in-memory or if encryption service is not available ++ async (v) => { ++ if (this._type === 'in-memory' || !await this._encryptionService.isEncryptionAvailable()) { ++ return v; ++ } ++ return this._encryptionService.encrypt(v); ++ }, + this._logService, + ); + } catch (e) { +@@ -194,8 +204,9 @@ export class BaseSecretStorageService ex + + private async initialize(): Promise { + let storageService; +- if (!this._useInMemoryStorage && await this._encryptionService.isEncryptionAvailable()) { +- this._logService.trace(`[SecretStorageService] Encryption is available, using persisted storage`); ++ if (!this._useInMemoryStorage) { ++ // Use persisted storage when not forced to use in-memory ++ this._logService.trace(`[SecretStorageService] Using persisted storage`); + this._type = 'persisted'; + storageService = this._storageService; + } else { +@@ -203,7 +214,7 @@ export class BaseSecretStorageService ex + if (this._type === 'in-memory') { + return this._storageService; + } +- this._logService.trace('[SecretStorageService] Encryption is not available, falling back to in-memory storage'); ++ this._logService.trace('[SecretStorageService] Falling back to in-memory storage'); + this._type = 'in-memory'; + storageService = this._register(new InMemoryStorageService()); + } +Index: code-server/lib/vscode/src/vs/workbench/services/secrets/browser/secretStorageService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/services/secrets/browser/secretStorageService.ts ++++ code-server/lib/vscode/src/vs/workbench/services/secrets/browser/secretStorageService.ts +@@ -22,9 +22,9 @@ export class BrowserSecretStorageService + @IBrowserWorkbenchEnvironmentService environmentService: IBrowserWorkbenchEnvironmentService, + @ILogService logService: ILogService + ) { +- // We don't have encryption in the browser so instead we use the +- // in-memory base class implementation instead. +- super(true, storageService, encryptionService, logService); ++ // Use remote storage if enabled, otherwise use in-memory storage ++ const useRemoteStorage = environmentService.options?.remoteStorageEnabled ?? false; ++ super(!useRemoteStorage, storageService, encryptionService, logService); + + if (environmentService.options?.secretStorageProvider) { + this._secretStorageProvider = environmentService.options.secretStorageProvider; +Index: code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/code/browser/workbench/workbench.ts ++++ code-server/lib/vscode/src/vs/code/browser/workbench/workbench.ts +@@ -625,2 +625,2 @@ +- secretStorageProvider: config.remoteAuthority && !secretStorageKeyPath +- ? undefined /* with a remote without embedder-preferred storage, store on the remote */ ++ secretStorageProvider: config.remoteStorageEnabled || (config.remoteAuthority && !secretStorageKeyPath) ++ ? undefined /* with remote storage enabled, or without embedder-prefered storage, store on the remote */ + }); + })(); diff --git a/patches/remote-storage.diff b/patches/remote-storage.diff new file mode 100644 index 000000000000..277db4514b18 --- /dev/null +++ b/patches/remote-storage.diff @@ -0,0 +1,1193 @@ +Merge https://github.com/microsoft/vscode/pull/288880/ +feat(server): add remote storage persistence for VS Code Web +Index: code-server/lib/vscode/src/vs/platform/storage/node/storageMain.ts +=================================================================== +--- /dev/null ++++ code-server/lib/vscode/src/vs/platform/storage/node/storageMain.ts +@@ -0,0 +1,438 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++import * as fs from 'fs'; ++import { top } from '../../../base/common/arrays.js'; ++import { DeferredPromise } from '../../../base/common/async.js'; ++import { Emitter, Event } from '../../../base/common/event.js'; ++import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; ++import { join } from '../../../base/common/path.js'; ++import { StopWatch } from '../../../base/common/stopwatch.js'; ++import { URI } from '../../../base/common/uri.js'; ++import { Promises } from '../../../base/node/pfs.js'; ++import { InMemoryStorageDatabase, IStorage, Storage, StorageHint, StorageState } from '../../../base/parts/storage/common/storage.js'; ++import { ISQLiteStorageDatabaseLoggingOptions, SQLiteStorageDatabase } from '../../../base/parts/storage/node/storage.js'; ++import { IEnvironmentService } from '../../environment/common/environment.js'; ++import { IFileService } from '../../files/common/files.js'; ++import { ILogService, LogLevel } from '../../log/common/log.js'; ++import { IS_NEW_KEY } from '../common/storage.js'; ++import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; ++import { currentSessionDateStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from '../../telemetry/common/telemetry.js'; ++import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier } from '../../workspace/common/workspace.js'; ++import { Schemas } from '../../../base/common/network.js'; ++ ++export interface IStorageMainOptions { ++ ++ /** ++ * If enabled, storage will not persist to disk ++ * but into memory. ++ */ ++ readonly useInMemoryStorage?: boolean; ++} ++ ++/** ++ * Provides access to application, profile and workspace storage from ++ * a Node.js process that owns storage connections. ++ */ ++export interface IStorageMain extends IDisposable { ++ ++ /** ++ * Emitted whenever data is updated or deleted. ++ */ ++ readonly onDidChangeStorage: Event; ++ ++ /** ++ * Emitted when the storage is closed. ++ */ ++ readonly onDidCloseStorage: Event; ++ ++ /** ++ * Access to all cached items of this storage service. ++ */ ++ readonly items: Map; ++ ++ /** ++ * Allows to join on the `init` call having completed ++ * to be able to safely use the storage. ++ */ ++ readonly whenInit: Promise; ++ ++ /** ++ * Provides access to the `IStorage` implementation which will be ++ * in-memory for as long as the storage has not been initialized. ++ */ ++ readonly storage: IStorage; ++ ++ /** ++ * The file path of the underlying storage file if any. ++ */ ++ readonly path: string | undefined; ++ ++ /** ++ * Required call to ensure the service can be used. ++ */ ++ init(): Promise; ++ ++ /** ++ * Retrieve an element stored with the given key from storage. Use ++ * the provided defaultValue if the element is null or undefined. ++ */ ++ get(key: string, fallbackValue: string): string; ++ get(key: string, fallbackValue?: string): string | undefined; ++ ++ /** ++ * Store a string value under the given key to storage. The value will ++ * be converted to a string. ++ */ ++ set(key: string, value: string | boolean | number | undefined | null): void; ++ ++ /** ++ * Delete an element stored under the provided key from storage. ++ */ ++ delete(key: string): void; ++ ++ /** ++ * Whether the storage is using in-memory persistence or not. ++ */ ++ isInMemory(): boolean; ++ ++ /** ++ * Attempts to reduce the DB size via optimization commands if supported. ++ */ ++ optimize(): Promise; ++ ++ /** ++ * Close the storage connection. ++ */ ++ close(): Promise; ++} ++ ++export interface IStorageChangeEvent { ++ readonly key: string; ++} ++ ++export abstract class BaseStorageMain extends Disposable implements IStorageMain { ++ ++ private static readonly LOG_SLOW_CLOSE_THRESHOLD = 2000; ++ ++ protected readonly _onDidChangeStorage = this._register(new Emitter()); ++ readonly onDidChangeStorage = this._onDidChangeStorage.event; ++ ++ private readonly _onDidCloseStorage = this._register(new Emitter()); ++ readonly onDidCloseStorage = this._onDidCloseStorage.event; ++ ++ private _storage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY })); // storage is in-memory until initialized ++ get storage(): IStorage { return this._storage; } ++ ++ abstract get path(): string | undefined; ++ ++ private initializePromise: Promise | undefined = undefined; ++ ++ private readonly whenInitPromise = new DeferredPromise(); ++ readonly whenInit = this.whenInitPromise.p; ++ ++ private state = StorageState.None; ++ ++ constructor( ++ protected readonly logService: ILogService, ++ private readonly fileService: IFileService ++ ) { ++ super(); ++ } ++ ++ isInMemory(): boolean { ++ return this._storage.isInMemory(); ++ } ++ ++ init(): Promise { ++ if (!this.initializePromise) { ++ this.initializePromise = (async () => { ++ if (this.state !== StorageState.None) { ++ return; // either closed or already initialized ++ } ++ ++ try { ++ ++ // Create storage via subclasses ++ const storage = this._register(await this.doCreate()); ++ ++ // Replace our in-memory storage with the real ++ // once as soon as possible without awaiting ++ // the init call. ++ this._storage.dispose(); ++ this._storage = storage; ++ ++ // Re-emit storage changes via event ++ this._register(storage.onDidChangeStorage(e => this._onDidChangeStorage.fire(e))); ++ ++ // Await storage init ++ await this.doInit(storage); ++ ++ // Ensure we track whether storage is new or not ++ const isNewStorage = storage.getBoolean(IS_NEW_KEY); ++ if (isNewStorage === undefined) { ++ storage.set(IS_NEW_KEY, true); ++ } else if (isNewStorage) { ++ storage.set(IS_NEW_KEY, false); ++ } ++ } catch (error) { ++ this.logService.error(`[storage main] initialize(): Unable to init storage due to ${error}`); ++ } finally { ++ ++ // Update state ++ this.state = StorageState.Initialized; ++ ++ // Mark init promise as completed ++ this.whenInitPromise.complete(); ++ } ++ })(); ++ } ++ ++ return this.initializePromise; ++ } ++ ++ protected createLoggingOptions(): ISQLiteStorageDatabaseLoggingOptions { ++ return { ++ logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined, ++ logError: error => this.logService.error(error) ++ }; ++ } ++ ++ protected doInit(storage: IStorage): Promise { ++ return storage.init(); ++ } ++ ++ protected abstract doCreate(): Promise; ++ ++ get items(): Map { return this._storage.items; } ++ ++ get(key: string, fallbackValue: string): string; ++ get(key: string, fallbackValue?: string): string | undefined; ++ get(key: string, fallbackValue?: string): string | undefined { ++ return this._storage.get(key, fallbackValue); ++ } ++ ++ set(key: string, value: string | boolean | number | undefined | null): Promise { ++ return this._storage.set(key, value); ++ } ++ ++ delete(key: string): Promise { ++ return this._storage.delete(key); ++ } ++ ++ optimize(): Promise { ++ return this._storage.optimize(); ++ } ++ ++ async close(): Promise { ++ ++ // Measure how long it takes to close storage ++ const watch = new StopWatch(false); ++ await this.doClose(); ++ watch.stop(); ++ ++ // If close() is taking a long time, there is ++ // a chance that the underlying DB is large ++ // either on disk or in general. In that case ++ // log some additional info to further diagnose ++ if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD) { ++ await this.logSlowClose(watch); ++ } ++ ++ // Signal as event ++ this._onDidCloseStorage.fire(); ++ } ++ ++ private async logSlowClose(watch: StopWatch) { ++ if (!this.path) { ++ return; ++ } ++ ++ try { ++ const largestEntries = top(Array.from(this._storage.items.entries()) ++ .map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5) ++ .map(entry => `${entry.key}:${entry.length}`).join(', '); ++ const dbSize = (await this.fileService.stat(URI.file(this.path))).size; ++ ++ this.logService.warn(`[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`); ++ } catch (error) { ++ this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); ++ } ++ } ++ ++ private async doClose(): Promise { ++ ++ // Ensure we are not accidentally leaving ++ // a pending initialized storage behind in ++ // case `close()` was called before `init()` ++ // finishes. ++ if (this.initializePromise) { ++ await this.initializePromise; ++ } ++ ++ // Update state ++ this.state = StorageState.Closed; ++ ++ // Propagate to storage lib ++ await this._storage.close(); ++ } ++} ++ ++class BaseProfileAwareStorageMain extends BaseStorageMain { ++ ++ private static readonly STORAGE_NAME = 'state.vscdb'; ++ ++ get path(): string | undefined { ++ if (!this.options.useInMemoryStorage) { ++ return join(this.profile.globalStorageHome.with({ scheme: Schemas.file }).fsPath, BaseProfileAwareStorageMain.STORAGE_NAME); ++ } ++ ++ return undefined; ++ } ++ ++ constructor( ++ private readonly profile: IUserDataProfile, ++ private readonly options: IStorageMainOptions, ++ logService: ILogService, ++ fileService: IFileService ++ ) { ++ super(logService, fileService); ++ } ++ ++ protected async doCreate(): Promise { ++ return new Storage(new SQLiteStorageDatabase(this.path ?? SQLiteStorageDatabase.IN_MEMORY_PATH, { ++ logging: this.createLoggingOptions() ++ }), !this.path ? { hint: StorageHint.STORAGE_IN_MEMORY } : undefined); ++ } ++} ++ ++export class ProfileStorageMain extends BaseProfileAwareStorageMain { ++ ++} ++ ++export class ApplicationStorageMain extends BaseProfileAwareStorageMain { ++ ++ constructor( ++ options: IStorageMainOptions, ++ userDataProfileService: IUserDataProfilesService, ++ logService: ILogService, ++ fileService: IFileService ++ ) { ++ super(userDataProfileService.defaultProfile, options, logService, fileService); ++ } ++ ++ protected override async doInit(storage: IStorage): Promise { ++ await super.doInit(storage); ++ ++ // Apply telemetry values as part of the application storage initialization ++ this.updateTelemetryState(storage); ++ } ++ ++ private updateTelemetryState(storage: IStorage): void { ++ ++ // First session date (once) ++ const firstSessionDate = storage.get(firstSessionDateStorageKey, undefined); ++ if (firstSessionDate === undefined) { ++ storage.set(firstSessionDateStorageKey, new Date().toUTCString()); ++ } ++ ++ // Last / current session (always) ++ // previous session date was the "current" one at that time ++ // current session date is "now" ++ const lastSessionDate = storage.get(currentSessionDateStorageKey, undefined); ++ const currentSessionDate = new Date().toUTCString(); ++ storage.set(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate); ++ storage.set(currentSessionDateStorageKey, currentSessionDate); ++ } ++} ++ ++export class WorkspaceStorageMain extends BaseStorageMain { ++ ++ private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; ++ private static readonly WORKSPACE_META_NAME = 'workspace.json'; ++ ++ get path(): string | undefined { ++ if (!this.options.useInMemoryStorage) { ++ return join(this.environmentService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, this.workspace.id, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); ++ } ++ ++ return undefined; ++ } ++ ++ constructor( ++ private workspace: IAnyWorkspaceIdentifier, ++ private readonly options: IStorageMainOptions, ++ logService: ILogService, ++ private readonly environmentService: IEnvironmentService, ++ fileService: IFileService ++ ) { ++ super(logService, fileService); ++ } ++ ++ protected async doCreate(): Promise { ++ const { storageFilePath, wasCreated } = await this.prepareWorkspaceStorageFolder(); ++ ++ return new Storage(new SQLiteStorageDatabase(storageFilePath, { ++ logging: this.createLoggingOptions() ++ }), { hint: this.options.useInMemoryStorage ? StorageHint.STORAGE_IN_MEMORY : wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined }); ++ } ++ ++ private async prepareWorkspaceStorageFolder(): Promise<{ storageFilePath: string; wasCreated: boolean }> { ++ ++ // Return early if using inMemory storage ++ if (this.options.useInMemoryStorage) { ++ return { storageFilePath: SQLiteStorageDatabase.IN_MEMORY_PATH, wasCreated: true }; ++ } ++ ++ // Otherwise, ensure the storage folder exists on disk ++ const workspaceStorageFolderPath = join(this.environmentService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, this.workspace.id); ++ const workspaceStorageDatabasePath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); ++ ++ const storageExists = await Promises.exists(workspaceStorageFolderPath); ++ if (storageExists) { ++ return { storageFilePath: workspaceStorageDatabasePath, wasCreated: false }; ++ } ++ ++ // Ensure storage folder exists ++ await fs.promises.mkdir(workspaceStorageFolderPath, { recursive: true }); ++ ++ // Write metadata into folder (but do not await) ++ this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); ++ ++ return { storageFilePath: workspaceStorageDatabasePath, wasCreated: true }; ++ } ++ ++ private async ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath: string): Promise { ++ let meta: object | undefined = undefined; ++ if (isSingleFolderWorkspaceIdentifier(this.workspace)) { ++ meta = { folder: this.workspace.uri.toString() }; ++ } else if (isWorkspaceIdentifier(this.workspace)) { ++ meta = { workspace: this.workspace.configPath.toString() }; ++ } ++ ++ if (meta) { ++ try { ++ const workspaceStorageMetaPath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_META_NAME); ++ const storageExists = await Promises.exists(workspaceStorageMetaPath); ++ if (!storageExists) { ++ await Promises.writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); ++ } ++ } catch (error) { ++ this.logService.error(`[storage main] ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`); ++ } ++ } ++ } ++} ++ ++export class InMemoryStorageMain extends BaseStorageMain { ++ ++ get path(): string | undefined { ++ return undefined; // in-memory has no path ++ } ++ ++ protected async doCreate(): Promise { ++ return new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }); ++ } ++} +Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts ++++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +@@ -65,6 +65,7 @@ export const serverOptions: OptionDescri + 'enable-sync': { type: 'boolean' }, + 'github-auth': { type: 'string' }, + 'use-test-resolver': { type: 'boolean' }, ++ 'enable-remote-storage': { type: 'boolean', cat: 'o', description: nls.localize('enable-remote-storage', 'Persist browser storage (settings, state, etc.) on the server instead of browser IndexedDB. Enables state portability across browsers/devices.') }, + + /* ----- extension management ----- */ + +@@ -206,6 +207,7 @@ export interface ServerParsedArgs { + 'enable-sync'?: boolean; + 'github-auth'?: string; + 'use-test-resolver'?: boolean; ++ 'enable-remote-storage'?: boolean; + + /* ----- extension management ----- */ + +Index: code-server/lib/vscode/src/vs/server/node/serverServices.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/serverServices.ts ++++ code-server/lib/vscode/src/vs/server/node/serverServices.ts +@@ -103,6 +103,8 @@ import { IMcpGalleryManifestService } fr + import { McpGalleryManifestIPCService } from '../../platform/mcp/common/mcpGalleryManifestServiceIpc.js'; + import { SANDBOX_HELPER_CHANNEL_NAME, SandboxHelperChannel } from '../../platform/sandbox/common/sandboxHelperIpc.js'; + import { SandboxHelperService } from '../../platform/sandbox/node/sandboxHelper.js'; ++import { IServerStorageMainService, ServerStorageMainService } from './storageMainService.js'; ++import { ServerStorageDatabaseChannel } from './serverStorageIpc.js'; + + const eventPrefix = 'monacoworkbench'; + +@@ -232,6 +234,7 @@ export async function setupServerService + services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); + services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService)); + services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService)); ++ services.set(IServerStorageMainService, new SyncDescriptor(ServerStorageMainService)); + + const instantiationService: IInstantiationService = new InstantiationService(services); + services.set(ILanguagePackService, instantiationService.createInstance(NativeLanguagePackService)); +@@ -304,6 +307,11 @@ export async function setupServerService + const languagePackChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); + socketServer.registerChannel('languagePacks', languagePackChannel); + ++ // Storage channel for remote storage persistence ++ const serverStorageMainService = accessor.get(IServerStorageMainService); ++ const storageChannel = disposables.add(new ServerStorageDatabaseChannel(logService, serverStorageMainService)); ++ socketServer.registerChannel('storage', storageChannel); ++ + // clean up extensions folder + remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp()); + +Index: code-server/lib/vscode/src/vs/server/node/serverStorageIpc.ts +=================================================================== +--- /dev/null ++++ code-server/lib/vscode/src/vs/server/node/serverStorageIpc.ts +@@ -0,0 +1,165 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++import { Emitter, Event } from '../../base/common/event.js'; ++import { Disposable } from '../../base/common/lifecycle.js'; ++import { revive } from '../../base/common/marshalling.js'; ++import { IServerChannel } from '../../base/parts/ipc/common/ipc.js'; ++import { ILogService } from '../../platform/log/common/log.js'; ++import { IBaseSerializableStorageRequest, ISerializableItemsChangeEvent, ISerializableUpdateRequest, Key, Value } from '../../platform/storage/common/storageIpc.js'; ++import { IStorageMain, IStorageChangeEvent } from '../../platform/storage/node/storageMain.js'; ++import { IServerStorageMainService } from './storageMainService.js'; ++import { IUserDataProfile } from '../../platform/userDataProfile/common/userDataProfile.js'; ++import { reviveIdentifier, IAnyWorkspaceIdentifier } from '../../platform/workspace/common/workspace.js'; ++ ++export class ServerStorageDatabaseChannel extends Disposable implements IServerChannel { ++ ++ private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; ++ ++ private readonly onDidChangeApplicationStorageEmitter = this._register(new Emitter()); ++ ++ private readonly mapProfileToOnDidChangeProfileStorageEmitter = new Map>(); ++ ++ constructor( ++ private readonly logService: ILogService, ++ private readonly storageMainService: IServerStorageMainService ++ ) { ++ super(); ++ ++ this.registerStorageChangeListeners(storageMainService.applicationStorage, this.onDidChangeApplicationStorageEmitter); ++ } ++ ++ //#region Storage Change Events ++ ++ private registerStorageChangeListeners(storage: IStorageMain, emitter: Emitter): void { ++ ++ // Listen for changes in provided storage to send to listeners ++ // that are listening. Use a debouncer to reduce IPC traffic. ++ ++ this._register(Event.debounce(storage.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { ++ if (!prev) { ++ prev = [cur]; ++ } else { ++ prev.push(cur); ++ } ++ ++ return prev; ++ }, ServerStorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => { ++ if (events.length) { ++ emitter.fire(this.serializeStorageChangeEvents(events, storage)); ++ } ++ })); ++ } ++ ++ private serializeStorageChangeEvents(events: IStorageChangeEvent[], storage: IStorageMain): ISerializableItemsChangeEvent { ++ const changed = new Map(); ++ const deleted = new Set(); ++ events.forEach(event => { ++ const existing = storage.get(event.key); ++ if (typeof existing === 'string') { ++ changed.set(event.key, existing); ++ } else { ++ deleted.add(event.key); ++ } ++ }); ++ ++ return { ++ changed: Array.from(changed.entries()), ++ deleted: Array.from(deleted.values()) ++ }; ++ } ++ ++ // eslint-disable-next-line @typescript-eslint/no-explicit-any ++ listen(_: unknown, event: string, arg: IBaseSerializableStorageRequest): Event { ++ switch (event) { ++ case 'onDidChangeStorage': { ++ const profile = arg.profile ? revive(arg.profile) : undefined; ++ ++ // Without profile: application scope ++ if (!profile) { ++ return this.onDidChangeApplicationStorageEmitter.event; ++ } ++ ++ // With profile: profile scope for the profile ++ let profileStorageChangeEmitter = this.mapProfileToOnDidChangeProfileStorageEmitter.get(profile.id); ++ if (!profileStorageChangeEmitter) { ++ profileStorageChangeEmitter = this._register(new Emitter()); ++ this.registerStorageChangeListeners(this.storageMainService.profileStorage(profile), profileStorageChangeEmitter); ++ this.mapProfileToOnDidChangeProfileStorageEmitter.set(profile.id, profileStorageChangeEmitter); ++ } ++ ++ return profileStorageChangeEmitter.event; ++ } ++ } ++ ++ throw new Error(`Event not found: ${event}`); ++ } ++ ++ //#endregion ++ ++ // eslint-disable-next-line @typescript-eslint/no-explicit-any ++ async call(_: unknown, command: string, arg: IBaseSerializableStorageRequest): Promise { ++ const profile = arg.profile ? revive(arg.profile) : undefined; ++ const workspace = reviveIdentifier(arg.workspace); ++ ++ // Get storage to be ready ++ const storage = await this.withStorageInitialized(profile, workspace); ++ ++ // handle call ++ switch (command) { ++ case 'getItems': { ++ return Array.from(storage.items.entries()); ++ } ++ ++ case 'updateItems': { ++ const items: ISerializableUpdateRequest = arg; ++ ++ if (items.insert) { ++ for (const [key, value] of items.insert) { ++ storage.set(key, value); ++ } ++ } ++ ++ items.delete?.forEach(key => storage.delete(key)); ++ ++ break; ++ } ++ ++ case 'optimize': { ++ return storage.optimize(); ++ } ++ ++ case 'isUsed': { ++ const path = arg.payload as string | undefined; ++ if (typeof path === 'string') { ++ return this.storageMainService.isUsed(path); ++ } ++ return false; ++ } ++ ++ default: ++ throw new Error(`Call not found: ${command}`); ++ } ++ } ++ ++ private async withStorageInitialized(profile: IUserDataProfile | undefined, workspace: IAnyWorkspaceIdentifier | undefined): Promise { ++ let storage: IStorageMain; ++ if (workspace) { ++ storage = this.storageMainService.workspaceStorage(workspace); ++ } else if (profile) { ++ storage = this.storageMainService.profileStorage(profile); ++ } else { ++ storage = this.storageMainService.applicationStorage; ++ } ++ ++ try { ++ await storage.init(); ++ } catch (error) { ++ this.logService.error(`ServerStorageIPC#init: Unable to init ${workspace ? 'workspace' : profile ? 'profile' : 'application'} storage due to ${error}`); ++ } ++ ++ return storage; ++ } ++} +Index: code-server/lib/vscode/src/vs/server/node/storageMainService.ts +=================================================================== +--- /dev/null ++++ code-server/lib/vscode/src/vs/server/node/storageMainService.ts +@@ -0,0 +1,205 @@ ++/*--------------------------------------------------------------------------------------------- ++ * Copyright (c) Microsoft Corporation. All rights reserved. ++ * Licensed under the MIT License. See License.txt in the project root for license information. ++ *--------------------------------------------------------------------------------------------*/ ++ ++import { Emitter, Event } from '../../base/common/event.js'; ++import { Disposable } from '../../base/common/lifecycle.js'; ++import { createDecorator } from '../../platform/instantiation/common/instantiation.js'; ++import { ILogService } from '../../platform/log/common/log.js'; ++import { IFileService } from '../../platform/files/common/files.js'; ++import { IEnvironmentService } from '../../platform/environment/common/environment.js'; ++import { IUserDataProfilesService, IUserDataProfile } from '../../platform/userDataProfile/common/userDataProfile.js'; ++import { isProfileUsingDefaultStorage } from '../../platform/storage/common/storage.js'; ++import { IStorageMain, IStorageMainOptions, IStorageChangeEvent, ApplicationStorageMain, ProfileStorageMain, WorkspaceStorageMain } from '../../platform/storage/node/storageMain.js'; ++import { IAnyWorkspaceIdentifier } from '../../platform/workspace/common/workspace.js'; ++ ++//#region Server Storage Main Service ++ ++export const IServerStorageMainService = createDecorator('serverStorageMainService'); ++ ++export interface IProfileStorageChangeEvent extends IStorageChangeEvent { ++ readonly storage: IStorageMain; ++ readonly profile: IUserDataProfile; ++} ++ ++export interface IServerStorageMainService { ++ ++ readonly _serviceBrand: undefined; ++ ++ /** ++ * Provides access to the application storage shared across all ++ * windows and all profiles. ++ */ ++ readonly applicationStorage: IStorageMain; ++ ++ /** ++ * Emitted whenever data is updated or deleted in profile scoped storage. ++ */ ++ readonly onDidChangeProfileStorage: Event; ++ ++ /** ++ * Provides access to the profile storage shared across all windows ++ * for the provided profile. ++ */ ++ profileStorage(profile: IUserDataProfile): IStorageMain; ++ ++ /** ++ * Provides access to the workspace storage specific to a single window. ++ */ ++ workspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorageMain; ++ ++ /** ++ * Checks if the provided path is currently in use for a storage database. ++ * ++ * @param path the path to the storage file or parent folder ++ */ ++ isUsed(path: string): boolean; ++} ++ ++export class ServerStorageMainService extends Disposable implements IServerStorageMainService { ++ ++ declare readonly _serviceBrand: undefined; ++ ++ private readonly _onDidChangeProfileStorage = this._register(new Emitter()); ++ readonly onDidChangeProfileStorage = this._onDidChangeProfileStorage.event; ++ ++ readonly applicationStorage: IStorageMain; ++ ++ constructor( ++ @ILogService private readonly logService: ILogService, ++ @IEnvironmentService private readonly environmentService: IEnvironmentService, ++ @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, ++ @IFileService private readonly fileService: IFileService ++ ) { ++ super(); ++ ++ this.applicationStorage = this._register(this.createApplicationStorage()); ++ ++ this.registerListeners(); ++ } ++ ++ protected getStorageOptions(): IStorageMainOptions { ++ return { ++ useInMemoryStorage: false ++ }; ++ } ++ ++ private registerListeners(): void { ++ // Initialize application storage immediately ++ this.applicationStorage.init(); ++ ++ // Listen for profile changes ++ this._register(this.userDataProfilesService.onDidChangeProfiles(e => { ++ // Create storage for added profiles ++ for (const profile of e.added) { ++ (async () => { ++ if (!(await this.fileService.exists(profile.globalStorageHome))) { ++ await this.fileService.createFolder(profile.globalStorageHome); ++ } ++ })(); ++ } ++ ++ // Close storage for removed profiles ++ for (const profile of e.removed) { ++ const storage = this.mapProfileToStorage.get(profile.id); ++ if (storage) { ++ storage.close(); ++ this.mapProfileToStorage.delete(profile.id); ++ } ++ } ++ })); ++ } ++ ++ //#region Application Storage ++ ++ private createApplicationStorage(): IStorageMain { ++ this.logService.trace(`ServerStorageMainService: creating application storage`); ++ ++ const applicationStorage = new ApplicationStorageMain(this.getStorageOptions(), this.userDataProfilesService, this.logService, this.fileService); ++ ++ this._register(Event.once(applicationStorage.onDidCloseStorage)(() => { ++ this.logService.trace(`ServerStorageMainService: closed application storage`); ++ })); ++ ++ return applicationStorage; ++ } ++ ++ //#endregion ++ ++ //#region Profile Storage ++ ++ private readonly mapProfileToStorage = new Map(); ++ ++ profileStorage(profile: IUserDataProfile): IStorageMain { ++ if (isProfileUsingDefaultStorage(profile)) { ++ return this.applicationStorage; // for profiles using default storage, use application storage ++ } ++ ++ let profileStorage = this.mapProfileToStorage.get(profile.id); ++ if (!profileStorage) { ++ this.logService.trace(`ServerStorageMainService: creating profile storage (${profile.name})`); ++ ++ profileStorage = this._register(this.createProfileStorage(profile)); ++ this.mapProfileToStorage.set(profile.id, profileStorage); ++ ++ const listener = this._register(profileStorage.onDidChangeStorage(e => this._onDidChangeProfileStorage.fire({ ++ ...e, ++ storage: profileStorage!, ++ profile ++ }))); ++ ++ this._register(Event.once(profileStorage.onDidCloseStorage)(() => { ++ this.logService.trace(`ServerStorageMainService: closed profile storage (${profile.name})`); ++ ++ this.mapProfileToStorage.delete(profile.id); ++ listener.dispose(); ++ })); ++ } ++ ++ return profileStorage; ++ } ++ ++ private createProfileStorage(profile: IUserDataProfile): IStorageMain { ++ return new ProfileStorageMain(profile, this.getStorageOptions(), this.logService, this.fileService); ++ } ++ ++ //#endregion ++ ++ ++ //#region Workspace Storage ++ ++ private readonly mapWorkspaceToStorage = new Map(); ++ ++ workspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorageMain { ++ let workspaceStorage = this.mapWorkspaceToStorage.get(workspace.id); ++ if (!workspaceStorage) { ++ this.logService.trace(`ServerStorageMainService: creating workspace storage (${workspace.id})`); ++ ++ workspaceStorage = this._register(this.createWorkspaceStorage(workspace)); ++ this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage); ++ ++ this._register(Event.once(workspaceStorage.onDidCloseStorage)(() => { ++ this.logService.trace(`ServerStorageMainService: closed workspace storage (${workspace.id})`); ++ ++ this.mapWorkspaceToStorage.delete(workspace.id); ++ })); ++ } ++ ++ return workspaceStorage; ++ } ++ ++ private createWorkspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorageMain { ++ return new WorkspaceStorageMain(workspace, this.getStorageOptions(), this.logService, this.environmentService, this.fileService); ++ } ++ ++ //#endregion ++ ++ isUsed(path: string): boolean { ++ // For server, we don't need to track file usage like the electron-main service ++ // This is primarily used for external file operations in the desktop app ++ return false; ++ } ++} ++ ++//#endregion +Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts ++++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts +@@ -411,7 +411,8 @@ export class WebClientServer { + folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), + workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), + productConfiguration, +- callbackRoute: callbackRoute ++ callbackRoute: callbackRoute, ++ remoteStorageEnabled: this._environmentService.args['enable-remote-storage'] ? true : undefined + }; + + const cookies = cookie.parse(req.headers.cookie || ''); +Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts ++++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts +@@ -411,6 +411,19 @@ export interface IWorkbenchConstructionO + + //#endregion + ++ ++ //#region Storage options ++ ++ /** ++ * When true and connected to a remote authority, persist storage ++ * (settings, state, etc.) on the remote server instead of browser IndexedDB. ++ * This enables state portability across browsers/devices. ++ * @default false ++ */ ++ readonly remoteStorageEnabled?: boolean; ++ ++ //#endregion ++ + } + + +Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts ++++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts +@@ -33,7 +33,7 @@ import { ConfigurationCache } from '../s + import { ISignService } from '../../platform/sign/common/sign.js'; + import { SignService } from '../../platform/sign/browser/signService.js'; + import { IWorkbenchConstructionOptions, IWorkbench, IWorkspace, ITunnel } from './web.api.js'; +-import { BrowserStorageService } from '../services/storage/browser/storageService.js'; ++import { BrowserStorageService, RemoteBrowserStorageService } from '../services/storage/browser/storageService.js'; + import { IStorageService } from '../../platform/storage/common/storage.js'; + import { toLocalISOString } from '../../base/common/date.js'; + import { isWorkspaceToOpen, isFolderToOpen } from '../../platform/window/common/window.js'; +@@ -475,1 +475,1 @@ +- this.createStorageService(workspace, logService, userDataProfileService).then(service => { ++ this.createStorageService(workspace, logService, userDataProfileService, remoteAgentService).then(service => { +@@ -597,7 +597,28 @@ + })); + } + +- protected async createStorageService(workspace: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService): Promise { ++ protected async createStorageService(workspace: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService, remoteAgentService: IRemoteAgentService): Promise { ++ // Use remote storage if enabled and connected to a remote server ++ const connection = remoteAgentService.getConnection(); ++ if (this.configuration.remoteStorageEnabled && connection) { ++ logService.info('Using remote storage service for browser state persistence'); ++ const storageService = new RemoteBrowserStorageService(workspace, userDataProfileService, connection, logService); ++ ++ try { ++ await storageService.initialize(); ++ ++ // Register to close on shutdown ++ this.onWillShutdownDisposables.add(toDisposable(() => storageService.close())); ++ ++ return storageService; ++ } catch (error) { ++ onUnexpectedError(error); ++ logService.error('Remote storage service initialization failed, falling back to browser storage', error); ++ // Fall through to browser storage on failure ++ } ++ } ++ ++ // Default: Use browser IndexedDB storage + const storageService = new BrowserStorageService(workspace, userDataProfileService, logService); + + try { +Index: code-server/lib/vscode/src/vs/sessions/browser/web.main.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/sessions/browser/web.main.ts ++++ code-server/lib/vscode/src/vs/sessions/browser/web.main.ts +@@ -85,1 +85,1 @@ +- const storageService = await this.createStorageService(workspaceIdentifier, logService, userDataProfileService); ++ const storageService = await this.createStorageService(workspaceIdentifier, logService, userDataProfileService, _remoteAgentService); + +Index: code-server/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts +=================================================================== +--- code-server.orig/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts ++++ code-server/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts +@@ -21,3 +21,5 @@ + import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js'; ++import { ApplicationStorageDatabaseClient, ProfileStorageDatabaseClient, WorkspaceStorageDatabaseClient } from '../../../../platform/storage/common/storageIpc.js'; ++import { IRemoteAgentConnection } from '../../remote/common/remoteAgentService.js'; + + export class BrowserStorageService extends AbstractStorageService { +@@ -460,3 +462,188 @@ export class IndexedDBStorageDatabase ex + await db.runInTransaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite', objectStore => objectStore.clear()); + } + } ++ ++/** ++ * Storage service that uses the remote server for persistence via IPC, ++ * instead of browser IndexedDB. Enabled via the `remoteStorageEnabled` option. ++ */ ++export class RemoteBrowserStorageService extends AbstractStorageService { ++ ++ // private readonly applicationStorageProfile: IUserDataProfile; ++ ++ private readonly applicationStorage: IStorage; ++ ++ private profileStorageProfile: IUserDataProfile; ++ private readonly profileStorageDisposables = this._register(new DisposableStore()); ++ private profileStorage: IStorage; ++ ++ private workspaceStorageId: string | undefined; ++ private readonly workspaceStorageDisposables = this._register(new DisposableStore()); ++ private workspaceStorage: IStorage | undefined; ++ ++ constructor( ++ private readonly workspace: IAnyWorkspaceIdentifier, ++ private readonly userDataProfileService: IUserDataProfileService, ++ private readonly connection: IRemoteAgentConnection, ++ @ILogService private readonly logService: ILogService, ++ ) { ++ super(); ++ ++ ++ this.applicationStorage = this.createApplicationStorage(); ++ ++ this.profileStorageProfile = this.userDataProfileService.currentProfile; ++ this.profileStorage = this.createProfileStorage(this.profileStorageProfile); ++ ++ this.workspaceStorageId = this.workspace.id; ++ this.workspaceStorage = this.createWorkspaceStorage(this.workspace); ++ ++ this.registerListeners(); ++ } ++ ++ private registerListeners(): void { ++ this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => e.join(this.switchToProfile(e.profile)))); ++ } ++ ++ private createApplicationStorage(): IStorage { ++ const storageDataBaseClient = this._register(new ApplicationStorageDatabaseClient(this.connection.getChannel('storage'))); ++ const applicationStorage = this._register(new Storage(storageDataBaseClient)); ++ ++ this._register(applicationStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.APPLICATION, e))); ++ ++ return applicationStorage; ++ } ++ ++ private createProfileStorage(profile: IUserDataProfile): IStorage { ++ ++ // First clear any previously associated disposables ++ this.profileStorageDisposables.clear(); ++ ++ // Remember profile associated to profile storage ++ this.profileStorageProfile = profile; ++ ++ let profileStorage: IStorage; ++ if (isProfileUsingDefaultStorage(profile)) { ++ ++ // If we are using default profile storage, the profile storage is ++ // actually the same as application storage. As such we ++ // avoid creating the storage library a second time on ++ // the same DB. ++ ++ profileStorage = this.applicationStorage; ++ } else { ++ const storageDataBaseClient = this.profileStorageDisposables.add(new ProfileStorageDatabaseClient(this.connection.getChannel('storage'), profile)); ++ profileStorage = this.profileStorageDisposables.add(new Storage(storageDataBaseClient)); ++ } ++ ++ this.profileStorageDisposables.add(profileStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.PROFILE, e))); ++ ++ return profileStorage; ++ } ++ ++ private createWorkspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorage | undefined { ++ ++ // First clear any previously associated disposables ++ this.workspaceStorageDisposables.clear(); ++ ++ // Remember workspace ID for logging later ++ this.workspaceStorageId = workspace?.id; ++ ++ let workspaceStorage: IStorage | undefined = undefined; ++ if (workspace) { ++ const storageDataBaseClient = this.workspaceStorageDisposables.add(new WorkspaceStorageDatabaseClient(this.connection.getChannel('storage'), workspace)); ++ workspaceStorage = this.workspaceStorageDisposables.add(new Storage(storageDataBaseClient)); ++ ++ this.workspaceStorageDisposables.add(workspaceStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.WORKSPACE, e))); ++ } ++ ++ return workspaceStorage; ++ } ++ ++ protected async doInitialize(): Promise { ++ ++ // Init all storage locations ++ await Promises.settled([ ++ this.applicationStorage.init(), ++ this.profileStorage.init(), ++ this.workspaceStorage?.init() ?? Promise.resolve() ++ ]); ++ } ++ ++ protected getStorage(scope: StorageScope): IStorage | undefined { ++ switch (scope) { ++ case StorageScope.APPLICATION: ++ return this.applicationStorage; ++ case StorageScope.PROFILE: ++ return this.profileStorage; ++ default: ++ return this.workspaceStorage; ++ } ++ } ++ ++ protected getLogDetails(scope: StorageScope): string | undefined { ++ switch (scope) { ++ case StorageScope.APPLICATION: ++ return 'Remote Application Storage'; ++ case StorageScope.PROFILE: ++ return `Remote Profile Storage (${this.profileStorageProfile.name})`; ++ default: ++ return this.workspaceStorageId ? `Remote Workspace Storage (${this.workspaceStorageId})` : undefined; ++ } ++ } ++ ++ protected async switchToProfile(toProfile: IUserDataProfile): Promise { ++ if (!this.canSwitchProfile(this.profileStorageProfile, toProfile)) { ++ return; ++ } ++ ++ const oldProfileStorage = this.profileStorage; ++ const oldItems = oldProfileStorage.items; ++ ++ // Close old profile storage but only if this is ++ // different from application storage! ++ if (oldProfileStorage !== this.applicationStorage) { ++ await oldProfileStorage.close(); ++ } ++ ++ // Create new profile storage & init ++ this.profileStorage = this.createProfileStorage(toProfile); ++ await this.profileStorage.init(); ++ ++ // Handle data switch and eventing ++ this.switchData(oldItems, this.profileStorage, StorageScope.PROFILE); ++ } ++ ++ protected async switchToWorkspace(toWorkspace: IAnyWorkspaceIdentifier, preserveData: boolean): Promise { ++ const oldWorkspaceStorage = this.workspaceStorage; ++ const oldItems = oldWorkspaceStorage?.items ?? new Map(); ++ ++ // Close old workspace storage ++ await oldWorkspaceStorage?.close(); ++ ++ // Create new workspace storage & init ++ this.workspaceStorage = this.createWorkspaceStorage(toWorkspace); ++ await this.workspaceStorage?.init(); ++ ++ // Handle data switch and eventing ++ if (this.workspaceStorage) { ++ this.switchData(oldItems, this.workspaceStorage, StorageScope.WORKSPACE); ++ } ++ } ++ ++ hasScope(scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean { ++ if (isUserDataProfile(scope)) { ++ return this.profileStorageProfile.id === scope.id; ++ } ++ ++ return this.workspaceStorageId === scope.id; ++ } ++ ++ close(): void { ++ this.logService.trace('RemoteBrowserStorageService#close()'); ++ ++ // Always dispose to ensure that no timeouts or callbacks ++ // get triggered in this phase. ++ this.dispose(); ++ } ++} +Index: code-server/src/node/cli.ts +=================================================================== +--- code-server.orig/src/node/cli.ts ++++ code-server/src/node/cli.ts +@@ -56,6 +56,7 @@ export interface UserProvidedCodeArgs { + "session-socket"?: string + "cookie-suffix"?: string + "link-protection-trusted-domains"?: string[] ++ "enable-remote-storage"?: boolean + // locale is used by both VS Code and code-server. + locale?: string + } +@@ -303,6 +304,11 @@ export const options: Options Date: Fri, 7 Aug 2026 07:34:40 +0000 Subject: [PATCH 02/12] refactor: reuse Electron storage service --- patches/remote-storage.diff | 1213 ++--------------------------------- 1 file changed, 57 insertions(+), 1156 deletions(-) diff --git a/patches/remote-storage.diff b/patches/remote-storage.diff index 277db4514b18..f4804fd64f5b 100644 --- a/patches/remote-storage.diff +++ b/patches/remote-storage.diff @@ -1,1193 +1,94 @@ -Merge https://github.com/microsoft/vscode/pull/288880/ -feat(server): add remote storage persistence for VS Code Web -Index: code-server/lib/vscode/src/vs/platform/storage/node/storageMain.ts -=================================================================== ---- /dev/null -+++ code-server/lib/vscode/src/vs/platform/storage/node/storageMain.ts -@@ -0,0 +1,438 @@ -+/*--------------------------------------------------------------------------------------------- -+ * Copyright (c) Microsoft Corporation. All rights reserved. -+ * Licensed under the MIT License. See License.txt in the project root for license information. -+ *--------------------------------------------------------------------------------------------*/ -+ -+import * as fs from 'fs'; -+import { top } from '../../../base/common/arrays.js'; -+import { DeferredPromise } from '../../../base/common/async.js'; -+import { Emitter, Event } from '../../../base/common/event.js'; -+import { Disposable, IDisposable } from '../../../base/common/lifecycle.js'; -+import { join } from '../../../base/common/path.js'; -+import { StopWatch } from '../../../base/common/stopwatch.js'; -+import { URI } from '../../../base/common/uri.js'; -+import { Promises } from '../../../base/node/pfs.js'; -+import { InMemoryStorageDatabase, IStorage, Storage, StorageHint, StorageState } from '../../../base/parts/storage/common/storage.js'; -+import { ISQLiteStorageDatabaseLoggingOptions, SQLiteStorageDatabase } from '../../../base/parts/storage/node/storage.js'; -+import { IEnvironmentService } from '../../environment/common/environment.js'; -+import { IFileService } from '../../files/common/files.js'; -+import { ILogService, LogLevel } from '../../log/common/log.js'; -+import { IS_NEW_KEY } from '../common/storage.js'; -+import { IUserDataProfile, IUserDataProfilesService } from '../../userDataProfile/common/userDataProfile.js'; -+import { currentSessionDateStorageKey, firstSessionDateStorageKey, lastSessionDateStorageKey } from '../../telemetry/common/telemetry.js'; -+import { isSingleFolderWorkspaceIdentifier, isWorkspaceIdentifier, IAnyWorkspaceIdentifier } from '../../workspace/common/workspace.js'; -+import { Schemas } from '../../../base/common/network.js'; -+ -+export interface IStorageMainOptions { -+ -+ /** -+ * If enabled, storage will not persist to disk -+ * but into memory. -+ */ -+ readonly useInMemoryStorage?: boolean; -+} -+ -+/** -+ * Provides access to application, profile and workspace storage from -+ * a Node.js process that owns storage connections. -+ */ -+export interface IStorageMain extends IDisposable { -+ -+ /** -+ * Emitted whenever data is updated or deleted. -+ */ -+ readonly onDidChangeStorage: Event; -+ -+ /** -+ * Emitted when the storage is closed. -+ */ -+ readonly onDidCloseStorage: Event; -+ -+ /** -+ * Access to all cached items of this storage service. -+ */ -+ readonly items: Map; -+ -+ /** -+ * Allows to join on the `init` call having completed -+ * to be able to safely use the storage. -+ */ -+ readonly whenInit: Promise; -+ -+ /** -+ * Provides access to the `IStorage` implementation which will be -+ * in-memory for as long as the storage has not been initialized. -+ */ -+ readonly storage: IStorage; -+ -+ /** -+ * The file path of the underlying storage file if any. -+ */ -+ readonly path: string | undefined; -+ -+ /** -+ * Required call to ensure the service can be used. -+ */ -+ init(): Promise; -+ -+ /** -+ * Retrieve an element stored with the given key from storage. Use -+ * the provided defaultValue if the element is null or undefined. -+ */ -+ get(key: string, fallbackValue: string): string; -+ get(key: string, fallbackValue?: string): string | undefined; -+ -+ /** -+ * Store a string value under the given key to storage. The value will -+ * be converted to a string. -+ */ -+ set(key: string, value: string | boolean | number | undefined | null): void; -+ -+ /** -+ * Delete an element stored under the provided key from storage. -+ */ -+ delete(key: string): void; -+ -+ /** -+ * Whether the storage is using in-memory persistence or not. -+ */ -+ isInMemory(): boolean; -+ -+ /** -+ * Attempts to reduce the DB size via optimization commands if supported. -+ */ -+ optimize(): Promise; -+ -+ /** -+ * Close the storage connection. -+ */ -+ close(): Promise; -+} -+ -+export interface IStorageChangeEvent { -+ readonly key: string; -+} -+ -+export abstract class BaseStorageMain extends Disposable implements IStorageMain { -+ -+ private static readonly LOG_SLOW_CLOSE_THRESHOLD = 2000; -+ -+ protected readonly _onDidChangeStorage = this._register(new Emitter()); -+ readonly onDidChangeStorage = this._onDidChangeStorage.event; -+ -+ private readonly _onDidCloseStorage = this._register(new Emitter()); -+ readonly onDidCloseStorage = this._onDidCloseStorage.event; -+ -+ private _storage = this._register(new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY })); // storage is in-memory until initialized -+ get storage(): IStorage { return this._storage; } -+ -+ abstract get path(): string | undefined; -+ -+ private initializePromise: Promise | undefined = undefined; -+ -+ private readonly whenInitPromise = new DeferredPromise(); -+ readonly whenInit = this.whenInitPromise.p; -+ -+ private state = StorageState.None; -+ -+ constructor( -+ protected readonly logService: ILogService, -+ private readonly fileService: IFileService -+ ) { -+ super(); -+ } -+ -+ isInMemory(): boolean { -+ return this._storage.isInMemory(); -+ } -+ -+ init(): Promise { -+ if (!this.initializePromise) { -+ this.initializePromise = (async () => { -+ if (this.state !== StorageState.None) { -+ return; // either closed or already initialized -+ } -+ -+ try { -+ -+ // Create storage via subclasses -+ const storage = this._register(await this.doCreate()); -+ -+ // Replace our in-memory storage with the real -+ // once as soon as possible without awaiting -+ // the init call. -+ this._storage.dispose(); -+ this._storage = storage; -+ -+ // Re-emit storage changes via event -+ this._register(storage.onDidChangeStorage(e => this._onDidChangeStorage.fire(e))); -+ -+ // Await storage init -+ await this.doInit(storage); -+ -+ // Ensure we track whether storage is new or not -+ const isNewStorage = storage.getBoolean(IS_NEW_KEY); -+ if (isNewStorage === undefined) { -+ storage.set(IS_NEW_KEY, true); -+ } else if (isNewStorage) { -+ storage.set(IS_NEW_KEY, false); -+ } -+ } catch (error) { -+ this.logService.error(`[storage main] initialize(): Unable to init storage due to ${error}`); -+ } finally { -+ -+ // Update state -+ this.state = StorageState.Initialized; -+ -+ // Mark init promise as completed -+ this.whenInitPromise.complete(); -+ } -+ })(); -+ } -+ -+ return this.initializePromise; -+ } -+ -+ protected createLoggingOptions(): ISQLiteStorageDatabaseLoggingOptions { -+ return { -+ logTrace: (this.logService.getLevel() === LogLevel.Trace) ? msg => this.logService.trace(msg) : undefined, -+ logError: error => this.logService.error(error) -+ }; -+ } -+ -+ protected doInit(storage: IStorage): Promise { -+ return storage.init(); -+ } -+ -+ protected abstract doCreate(): Promise; -+ -+ get items(): Map { return this._storage.items; } -+ -+ get(key: string, fallbackValue: string): string; -+ get(key: string, fallbackValue?: string): string | undefined; -+ get(key: string, fallbackValue?: string): string | undefined { -+ return this._storage.get(key, fallbackValue); -+ } -+ -+ set(key: string, value: string | boolean | number | undefined | null): Promise { -+ return this._storage.set(key, value); -+ } -+ -+ delete(key: string): Promise { -+ return this._storage.delete(key); -+ } -+ -+ optimize(): Promise { -+ return this._storage.optimize(); -+ } -+ -+ async close(): Promise { -+ -+ // Measure how long it takes to close storage -+ const watch = new StopWatch(false); -+ await this.doClose(); -+ watch.stop(); -+ -+ // If close() is taking a long time, there is -+ // a chance that the underlying DB is large -+ // either on disk or in general. In that case -+ // log some additional info to further diagnose -+ if (watch.elapsed() > BaseStorageMain.LOG_SLOW_CLOSE_THRESHOLD) { -+ await this.logSlowClose(watch); -+ } -+ -+ // Signal as event -+ this._onDidCloseStorage.fire(); -+ } -+ -+ private async logSlowClose(watch: StopWatch) { -+ if (!this.path) { -+ return; -+ } -+ -+ try { -+ const largestEntries = top(Array.from(this._storage.items.entries()) -+ .map(([key, value]) => ({ key, length: value.length })), (entryA, entryB) => entryB.length - entryA.length, 5) -+ .map(entry => `${entry.key}:${entry.length}`).join(', '); -+ const dbSize = (await this.fileService.stat(URI.file(this.path))).size; -+ -+ this.logService.warn(`[storage main] detected slow close() operation: Time: ${watch.elapsed()}ms, DB size: ${dbSize}b, Large Keys: ${largestEntries}`); -+ } catch (error) { -+ this.logService.error('[storage main] figuring out stats for slow DB on close() resulted in an error', error); -+ } -+ } -+ -+ private async doClose(): Promise { -+ -+ // Ensure we are not accidentally leaving -+ // a pending initialized storage behind in -+ // case `close()` was called before `init()` -+ // finishes. -+ if (this.initializePromise) { -+ await this.initializePromise; -+ } -+ -+ // Update state -+ this.state = StorageState.Closed; -+ -+ // Propagate to storage lib -+ await this._storage.close(); -+ } -+} -+ -+class BaseProfileAwareStorageMain extends BaseStorageMain { -+ -+ private static readonly STORAGE_NAME = 'state.vscdb'; -+ -+ get path(): string | undefined { -+ if (!this.options.useInMemoryStorage) { -+ return join(this.profile.globalStorageHome.with({ scheme: Schemas.file }).fsPath, BaseProfileAwareStorageMain.STORAGE_NAME); -+ } -+ -+ return undefined; -+ } -+ -+ constructor( -+ private readonly profile: IUserDataProfile, -+ private readonly options: IStorageMainOptions, -+ logService: ILogService, -+ fileService: IFileService -+ ) { -+ super(logService, fileService); -+ } -+ -+ protected async doCreate(): Promise { -+ return new Storage(new SQLiteStorageDatabase(this.path ?? SQLiteStorageDatabase.IN_MEMORY_PATH, { -+ logging: this.createLoggingOptions() -+ }), !this.path ? { hint: StorageHint.STORAGE_IN_MEMORY } : undefined); -+ } -+} -+ -+export class ProfileStorageMain extends BaseProfileAwareStorageMain { -+ -+} -+ -+export class ApplicationStorageMain extends BaseProfileAwareStorageMain { -+ -+ constructor( -+ options: IStorageMainOptions, -+ userDataProfileService: IUserDataProfilesService, -+ logService: ILogService, -+ fileService: IFileService -+ ) { -+ super(userDataProfileService.defaultProfile, options, logService, fileService); -+ } -+ -+ protected override async doInit(storage: IStorage): Promise { -+ await super.doInit(storage); -+ -+ // Apply telemetry values as part of the application storage initialization -+ this.updateTelemetryState(storage); -+ } -+ -+ private updateTelemetryState(storage: IStorage): void { -+ -+ // First session date (once) -+ const firstSessionDate = storage.get(firstSessionDateStorageKey, undefined); -+ if (firstSessionDate === undefined) { -+ storage.set(firstSessionDateStorageKey, new Date().toUTCString()); -+ } -+ -+ // Last / current session (always) -+ // previous session date was the "current" one at that time -+ // current session date is "now" -+ const lastSessionDate = storage.get(currentSessionDateStorageKey, undefined); -+ const currentSessionDate = new Date().toUTCString(); -+ storage.set(lastSessionDateStorageKey, typeof lastSessionDate === 'undefined' ? null : lastSessionDate); -+ storage.set(currentSessionDateStorageKey, currentSessionDate); -+ } -+} -+ -+export class WorkspaceStorageMain extends BaseStorageMain { -+ -+ private static readonly WORKSPACE_STORAGE_NAME = 'state.vscdb'; -+ private static readonly WORKSPACE_META_NAME = 'workspace.json'; -+ -+ get path(): string | undefined { -+ if (!this.options.useInMemoryStorage) { -+ return join(this.environmentService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, this.workspace.id, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); -+ } -+ -+ return undefined; -+ } -+ -+ constructor( -+ private workspace: IAnyWorkspaceIdentifier, -+ private readonly options: IStorageMainOptions, -+ logService: ILogService, -+ private readonly environmentService: IEnvironmentService, -+ fileService: IFileService -+ ) { -+ super(logService, fileService); -+ } -+ -+ protected async doCreate(): Promise { -+ const { storageFilePath, wasCreated } = await this.prepareWorkspaceStorageFolder(); -+ -+ return new Storage(new SQLiteStorageDatabase(storageFilePath, { -+ logging: this.createLoggingOptions() -+ }), { hint: this.options.useInMemoryStorage ? StorageHint.STORAGE_IN_MEMORY : wasCreated ? StorageHint.STORAGE_DOES_NOT_EXIST : undefined }); -+ } -+ -+ private async prepareWorkspaceStorageFolder(): Promise<{ storageFilePath: string; wasCreated: boolean }> { -+ -+ // Return early if using inMemory storage -+ if (this.options.useInMemoryStorage) { -+ return { storageFilePath: SQLiteStorageDatabase.IN_MEMORY_PATH, wasCreated: true }; -+ } -+ -+ // Otherwise, ensure the storage folder exists on disk -+ const workspaceStorageFolderPath = join(this.environmentService.workspaceStorageHome.with({ scheme: Schemas.file }).fsPath, this.workspace.id); -+ const workspaceStorageDatabasePath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_STORAGE_NAME); -+ -+ const storageExists = await Promises.exists(workspaceStorageFolderPath); -+ if (storageExists) { -+ return { storageFilePath: workspaceStorageDatabasePath, wasCreated: false }; -+ } -+ -+ // Ensure storage folder exists -+ await fs.promises.mkdir(workspaceStorageFolderPath, { recursive: true }); -+ -+ // Write metadata into folder (but do not await) -+ this.ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath); -+ -+ return { storageFilePath: workspaceStorageDatabasePath, wasCreated: true }; -+ } -+ -+ private async ensureWorkspaceStorageFolderMeta(workspaceStorageFolderPath: string): Promise { -+ let meta: object | undefined = undefined; -+ if (isSingleFolderWorkspaceIdentifier(this.workspace)) { -+ meta = { folder: this.workspace.uri.toString() }; -+ } else if (isWorkspaceIdentifier(this.workspace)) { -+ meta = { workspace: this.workspace.configPath.toString() }; -+ } -+ -+ if (meta) { -+ try { -+ const workspaceStorageMetaPath = join(workspaceStorageFolderPath, WorkspaceStorageMain.WORKSPACE_META_NAME); -+ const storageExists = await Promises.exists(workspaceStorageMetaPath); -+ if (!storageExists) { -+ await Promises.writeFile(workspaceStorageMetaPath, JSON.stringify(meta, undefined, 2)); -+ } -+ } catch (error) { -+ this.logService.error(`[storage main] ensureWorkspaceStorageFolderMeta(): Unable to create workspace storage metadata due to ${error}`); -+ } -+ } -+ } -+} -+ -+export class InMemoryStorageMain extends BaseStorageMain { -+ -+ get path(): string | undefined { -+ return undefined; // in-memory has no path -+ } -+ -+ protected async doCreate(): Promise { -+ return new Storage(new InMemoryStorageDatabase(), { hint: StorageHint.STORAGE_IN_MEMORY }); -+ } -+} -Index: code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts -=================================================================== +Reuse VS Code's Electron storage service for remote persistence + +--- code-server.orig/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts ++++ code-server/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts +@@ -6 +6 @@ +-import { IMainProcessService } from '../../../../platform/ipc/common/mainProcessService.js'; ++import { IRemoteService } from '../../../../platform/ipc/common/services.js'; +@@ -22 +22 @@ +- mainProcessService: IMainProcessService, ++ remoteService: Pick, +@@ -25 +25 @@ +- super(workspace, { currentProfile: userDataProfileService.currentProfile, defaultProfile: userDataProfilesService.defaultProfile }, mainProcessService, workbenchEnvironmentService); ++ super(workspace, { currentProfile: userDataProfileService.currentProfile, defaultProfile: userDataProfilesService.defaultProfile }, remoteService as IRemoteService, workbenchEnvironmentService); --- code-server.orig/lib/vscode/src/vs/server/node/serverEnvironmentService.ts +++ code-server/lib/vscode/src/vs/server/node/serverEnvironmentService.ts -@@ -65,6 +65,7 @@ export const serverOptions: OptionDescri - 'enable-sync': { type: 'boolean' }, - 'github-auth': { type: 'string' }, - 'use-test-resolver': { type: 'boolean' }, +@@ -68,0 +69 @@ + 'enable-remote-storage': { type: 'boolean', cat: 'o', description: nls.localize('enable-remote-storage', 'Persist browser storage (settings, state, etc.) on the server instead of browser IndexedDB. Enables state portability across browsers/devices.') }, - - /* ----- extension management ----- */ - -@@ -206,6 +207,7 @@ export interface ServerParsedArgs { - 'enable-sync'?: boolean; - 'github-auth'?: string; - 'use-test-resolver'?: boolean; +@@ -214,0 +216 @@ + 'enable-remote-storage'?: boolean; - - /* ----- extension management ----- */ - -Index: code-server/lib/vscode/src/vs/server/node/serverServices.ts -=================================================================== --- code-server.orig/lib/vscode/src/vs/server/node/serverServices.ts +++ code-server/lib/vscode/src/vs/server/node/serverServices.ts -@@ -103,6 +103,8 @@ import { IMcpGalleryManifestService } fr - import { McpGalleryManifestIPCService } from '../../platform/mcp/common/mcpGalleryManifestServiceIpc.js'; - import { SANDBOX_HELPER_CHANNEL_NAME, SandboxHelperChannel } from '../../platform/sandbox/common/sandboxHelperIpc.js'; - import { SandboxHelperService } from '../../platform/sandbox/node/sandboxHelper.js'; -+import { IServerStorageMainService, ServerStorageMainService } from './storageMainService.js'; -+import { ServerStorageDatabaseChannel } from './serverStorageIpc.js'; - - const eventPrefix = 'monacoworkbench'; - -@@ -232,6 +234,7 @@ export async function setupServerService - services.set(INativeServerExtensionManagementService, new SyncDescriptor(ExtensionManagementService)); - services.set(INativeMcpDiscoveryHelperService, new SyncDescriptor(NativeMcpDiscoveryHelperService)); - services.set(IMcpGatewayService, new SyncDescriptor(McpGatewayService)); -+ services.set(IServerStorageMainService, new SyncDescriptor(ServerStorageMainService)); - - const instantiationService: IInstantiationService = new InstantiationService(services); - services.set(ILanguagePackService, instantiationService.createInstance(NativeLanguagePackService)); -@@ -304,6 +307,11 @@ export async function setupServerService - const languagePackChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); - socketServer.registerChannel('languagePacks', languagePackChannel); - -+ // Storage channel for remote storage persistence -+ const serverStorageMainService = accessor.get(IServerStorageMainService); -+ const storageChannel = disposables.add(new ServerStorageDatabaseChannel(logService, serverStorageMainService)); -+ socketServer.registerChannel('storage', storageChannel); -+ - // clean up extensions folder - remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp()); - -Index: code-server/lib/vscode/src/vs/server/node/serverStorageIpc.ts -=================================================================== ---- /dev/null -+++ code-server/lib/vscode/src/vs/server/node/serverStorageIpc.ts -@@ -0,0 +1,165 @@ -+/*--------------------------------------------------------------------------------------------- -+ * Copyright (c) Microsoft Corporation. All rights reserved. -+ * Licensed under the MIT License. See License.txt in the project root for license information. -+ *--------------------------------------------------------------------------------------------*/ -+ -+import { Emitter, Event } from '../../base/common/event.js'; -+import { Disposable } from '../../base/common/lifecycle.js'; -+import { revive } from '../../base/common/marshalling.js'; -+import { IServerChannel } from '../../base/parts/ipc/common/ipc.js'; -+import { ILogService } from '../../platform/log/common/log.js'; -+import { IBaseSerializableStorageRequest, ISerializableItemsChangeEvent, ISerializableUpdateRequest, Key, Value } from '../../platform/storage/common/storageIpc.js'; -+import { IStorageMain, IStorageChangeEvent } from '../../platform/storage/node/storageMain.js'; -+import { IServerStorageMainService } from './storageMainService.js'; -+import { IUserDataProfile } from '../../platform/userDataProfile/common/userDataProfile.js'; -+import { reviveIdentifier, IAnyWorkspaceIdentifier } from '../../platform/workspace/common/workspace.js'; -+ -+export class ServerStorageDatabaseChannel extends Disposable implements IServerChannel { -+ -+ private static readonly STORAGE_CHANGE_DEBOUNCE_TIME = 100; -+ -+ private readonly onDidChangeApplicationStorageEmitter = this._register(new Emitter()); -+ -+ private readonly mapProfileToOnDidChangeProfileStorageEmitter = new Map>(); -+ -+ constructor( -+ private readonly logService: ILogService, -+ private readonly storageMainService: IServerStorageMainService -+ ) { -+ super(); -+ -+ this.registerStorageChangeListeners(storageMainService.applicationStorage, this.onDidChangeApplicationStorageEmitter); -+ } -+ -+ //#region Storage Change Events -+ -+ private registerStorageChangeListeners(storage: IStorageMain, emitter: Emitter): void { -+ -+ // Listen for changes in provided storage to send to listeners -+ // that are listening. Use a debouncer to reduce IPC traffic. -+ -+ this._register(Event.debounce(storage.onDidChangeStorage, (prev: IStorageChangeEvent[] | undefined, cur: IStorageChangeEvent) => { -+ if (!prev) { -+ prev = [cur]; -+ } else { -+ prev.push(cur); -+ } -+ -+ return prev; -+ }, ServerStorageDatabaseChannel.STORAGE_CHANGE_DEBOUNCE_TIME)(events => { -+ if (events.length) { -+ emitter.fire(this.serializeStorageChangeEvents(events, storage)); -+ } -+ })); -+ } -+ -+ private serializeStorageChangeEvents(events: IStorageChangeEvent[], storage: IStorageMain): ISerializableItemsChangeEvent { -+ const changed = new Map(); -+ const deleted = new Set(); -+ events.forEach(event => { -+ const existing = storage.get(event.key); -+ if (typeof existing === 'string') { -+ changed.set(event.key, existing); -+ } else { -+ deleted.add(event.key); -+ } -+ }); -+ -+ return { -+ changed: Array.from(changed.entries()), -+ deleted: Array.from(deleted.values()) -+ }; -+ } -+ -+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -+ listen(_: unknown, event: string, arg: IBaseSerializableStorageRequest): Event { -+ switch (event) { -+ case 'onDidChangeStorage': { -+ const profile = arg.profile ? revive(arg.profile) : undefined; -+ -+ // Without profile: application scope -+ if (!profile) { -+ return this.onDidChangeApplicationStorageEmitter.event; -+ } -+ -+ // With profile: profile scope for the profile -+ let profileStorageChangeEmitter = this.mapProfileToOnDidChangeProfileStorageEmitter.get(profile.id); -+ if (!profileStorageChangeEmitter) { -+ profileStorageChangeEmitter = this._register(new Emitter()); -+ this.registerStorageChangeListeners(this.storageMainService.profileStorage(profile), profileStorageChangeEmitter); -+ this.mapProfileToOnDidChangeProfileStorageEmitter.set(profile.id, profileStorageChangeEmitter); -+ } -+ -+ return profileStorageChangeEmitter.event; -+ } -+ } -+ -+ throw new Error(`Event not found: ${event}`); -+ } -+ -+ //#endregion -+ -+ // eslint-disable-next-line @typescript-eslint/no-explicit-any -+ async call(_: unknown, command: string, arg: IBaseSerializableStorageRequest): Promise { -+ const profile = arg.profile ? revive(arg.profile) : undefined; -+ const workspace = reviveIdentifier(arg.workspace); -+ -+ // Get storage to be ready -+ const storage = await this.withStorageInitialized(profile, workspace); -+ -+ // handle call -+ switch (command) { -+ case 'getItems': { -+ return Array.from(storage.items.entries()); -+ } -+ -+ case 'updateItems': { -+ const items: ISerializableUpdateRequest = arg; -+ -+ if (items.insert) { -+ for (const [key, value] of items.insert) { -+ storage.set(key, value); -+ } -+ } -+ -+ items.delete?.forEach(key => storage.delete(key)); -+ -+ break; -+ } -+ -+ case 'optimize': { -+ return storage.optimize(); -+ } -+ -+ case 'isUsed': { -+ const path = arg.payload as string | undefined; -+ if (typeof path === 'string') { -+ return this.storageMainService.isUsed(path); -+ } -+ return false; -+ } -+ -+ default: -+ throw new Error(`Call not found: ${command}`); -+ } -+ } -+ -+ private async withStorageInitialized(profile: IUserDataProfile | undefined, workspace: IAnyWorkspaceIdentifier | undefined): Promise { -+ let storage: IStorageMain; -+ if (workspace) { -+ storage = this.storageMainService.workspaceStorage(workspace); -+ } else if (profile) { -+ storage = this.storageMainService.profileStorage(profile); -+ } else { -+ storage = this.storageMainService.applicationStorage; -+ } -+ -+ try { -+ await storage.init(); -+ } catch (error) { -+ this.logService.error(`ServerStorageIPC#init: Unable to init ${workspace ? 'workspace' : profile ? 'profile' : 'application'} storage due to ${error}`); -+ } -+ -+ return storage; -+ } -+} -Index: code-server/lib/vscode/src/vs/server/node/storageMainService.ts -=================================================================== ---- /dev/null -+++ code-server/lib/vscode/src/vs/server/node/storageMainService.ts -@@ -0,0 +1,205 @@ -+/*--------------------------------------------------------------------------------------------- -+ * Copyright (c) Microsoft Corporation. All rights reserved. -+ * Licensed under the MIT License. See License.txt in the project root for license information. -+ *--------------------------------------------------------------------------------------------*/ -+ -+import { Emitter, Event } from '../../base/common/event.js'; -+import { Disposable } from '../../base/common/lifecycle.js'; -+import { createDecorator } from '../../platform/instantiation/common/instantiation.js'; -+import { ILogService } from '../../platform/log/common/log.js'; -+import { IFileService } from '../../platform/files/common/files.js'; -+import { IEnvironmentService } from '../../platform/environment/common/environment.js'; -+import { IUserDataProfilesService, IUserDataProfile } from '../../platform/userDataProfile/common/userDataProfile.js'; -+import { isProfileUsingDefaultStorage } from '../../platform/storage/common/storage.js'; -+import { IStorageMain, IStorageMainOptions, IStorageChangeEvent, ApplicationStorageMain, ProfileStorageMain, WorkspaceStorageMain } from '../../platform/storage/node/storageMain.js'; -+import { IAnyWorkspaceIdentifier } from '../../platform/workspace/common/workspace.js'; -+ -+//#region Server Storage Main Service -+ -+export const IServerStorageMainService = createDecorator('serverStorageMainService'); -+ -+export interface IProfileStorageChangeEvent extends IStorageChangeEvent { -+ readonly storage: IStorageMain; -+ readonly profile: IUserDataProfile; -+} -+ -+export interface IServerStorageMainService { -+ -+ readonly _serviceBrand: undefined; -+ -+ /** -+ * Provides access to the application storage shared across all -+ * windows and all profiles. -+ */ -+ readonly applicationStorage: IStorageMain; -+ -+ /** -+ * Emitted whenever data is updated or deleted in profile scoped storage. -+ */ -+ readonly onDidChangeProfileStorage: Event; -+ -+ /** -+ * Provides access to the profile storage shared across all windows -+ * for the provided profile. -+ */ -+ profileStorage(profile: IUserDataProfile): IStorageMain; -+ -+ /** -+ * Provides access to the workspace storage specific to a single window. -+ */ -+ workspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorageMain; -+ -+ /** -+ * Checks if the provided path is currently in use for a storage database. -+ * -+ * @param path the path to the storage file or parent folder -+ */ -+ isUsed(path: string): boolean; -+} -+ -+export class ServerStorageMainService extends Disposable implements IServerStorageMainService { -+ -+ declare readonly _serviceBrand: undefined; -+ -+ private readonly _onDidChangeProfileStorage = this._register(new Emitter()); -+ readonly onDidChangeProfileStorage = this._onDidChangeProfileStorage.event; -+ -+ readonly applicationStorage: IStorageMain; -+ -+ constructor( -+ @ILogService private readonly logService: ILogService, -+ @IEnvironmentService private readonly environmentService: IEnvironmentService, -+ @IUserDataProfilesService private readonly userDataProfilesService: IUserDataProfilesService, -+ @IFileService private readonly fileService: IFileService -+ ) { -+ super(); -+ -+ this.applicationStorage = this._register(this.createApplicationStorage()); -+ -+ this.registerListeners(); -+ } -+ -+ protected getStorageOptions(): IStorageMainOptions { -+ return { -+ useInMemoryStorage: false -+ }; -+ } -+ -+ private registerListeners(): void { -+ // Initialize application storage immediately -+ this.applicationStorage.init(); -+ -+ // Listen for profile changes -+ this._register(this.userDataProfilesService.onDidChangeProfiles(e => { -+ // Create storage for added profiles -+ for (const profile of e.added) { -+ (async () => { -+ if (!(await this.fileService.exists(profile.globalStorageHome))) { -+ await this.fileService.createFolder(profile.globalStorageHome); -+ } -+ })(); -+ } -+ -+ // Close storage for removed profiles -+ for (const profile of e.removed) { -+ const storage = this.mapProfileToStorage.get(profile.id); -+ if (storage) { -+ storage.close(); -+ this.mapProfileToStorage.delete(profile.id); -+ } -+ } -+ })); -+ } -+ -+ //#region Application Storage -+ -+ private createApplicationStorage(): IStorageMain { -+ this.logService.trace(`ServerStorageMainService: creating application storage`); -+ -+ const applicationStorage = new ApplicationStorageMain(this.getStorageOptions(), this.userDataProfilesService, this.logService, this.fileService); -+ -+ this._register(Event.once(applicationStorage.onDidCloseStorage)(() => { -+ this.logService.trace(`ServerStorageMainService: closed application storage`); -+ })); -+ -+ return applicationStorage; -+ } -+ -+ //#endregion -+ -+ //#region Profile Storage -+ -+ private readonly mapProfileToStorage = new Map(); -+ -+ profileStorage(profile: IUserDataProfile): IStorageMain { -+ if (isProfileUsingDefaultStorage(profile)) { -+ return this.applicationStorage; // for profiles using default storage, use application storage -+ } -+ -+ let profileStorage = this.mapProfileToStorage.get(profile.id); -+ if (!profileStorage) { -+ this.logService.trace(`ServerStorageMainService: creating profile storage (${profile.name})`); -+ -+ profileStorage = this._register(this.createProfileStorage(profile)); -+ this.mapProfileToStorage.set(profile.id, profileStorage); -+ -+ const listener = this._register(profileStorage.onDidChangeStorage(e => this._onDidChangeProfileStorage.fire({ -+ ...e, -+ storage: profileStorage!, -+ profile -+ }))); -+ -+ this._register(Event.once(profileStorage.onDidCloseStorage)(() => { -+ this.logService.trace(`ServerStorageMainService: closed profile storage (${profile.name})`); -+ -+ this.mapProfileToStorage.delete(profile.id); -+ listener.dispose(); -+ })); -+ } -+ -+ return profileStorage; -+ } -+ -+ private createProfileStorage(profile: IUserDataProfile): IStorageMain { -+ return new ProfileStorageMain(profile, this.getStorageOptions(), this.logService, this.fileService); -+ } -+ -+ //#endregion -+ -+ -+ //#region Workspace Storage -+ -+ private readonly mapWorkspaceToStorage = new Map(); -+ -+ workspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorageMain { -+ let workspaceStorage = this.mapWorkspaceToStorage.get(workspace.id); -+ if (!workspaceStorage) { -+ this.logService.trace(`ServerStorageMainService: creating workspace storage (${workspace.id})`); -+ -+ workspaceStorage = this._register(this.createWorkspaceStorage(workspace)); -+ this.mapWorkspaceToStorage.set(workspace.id, workspaceStorage); -+ -+ this._register(Event.once(workspaceStorage.onDidCloseStorage)(() => { -+ this.logService.trace(`ServerStorageMainService: closed workspace storage (${workspace.id})`); -+ -+ this.mapWorkspaceToStorage.delete(workspace.id); -+ })); -+ } -+ -+ return workspaceStorage; -+ } -+ -+ private createWorkspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorageMain { -+ return new WorkspaceStorageMain(workspace, this.getStorageOptions(), this.logService, this.environmentService, this.fileService); -+ } -+ -+ //#endregion -+ -+ isUsed(path: string): boolean { -+ // For server, we don't need to track file usage like the electron-main service -+ // This is primarily used for external file operations in the desktop app -+ return false; -+ } -+} -+ -+//#endregion -Index: code-server/lib/vscode/src/vs/server/node/webClientServer.ts -=================================================================== +@@ -107,0 +108,4 @@ ++import { StorageDatabaseChannel } from '../../platform/storage/electron-main/storageIpc.js'; ++import { StorageMainService } from '../../platform/storage/electron-main/storageMainService.js'; ++import type { ILifecycleMainService } from '../../platform/lifecycle/electron-main/lifecycleMainService.js'; ++import type { IUserDataProfilesMainService } from '../../platform/userDataProfile/electron-main/userDataProfile.js'; +@@ -240,0 +245,15 @@ ++ // Reuse the Electron storage service on the remote. The server has no ++ // windows, so its lifecycle is always ready and never loads a window. ++ const storageMainService = disposables.add(new StorageMainService( ++ logService, ++ environmentService, ++ userDataProfilesService as unknown as IUserDataProfilesMainService, ++ { ++ when: () => Promise.resolve(), ++ onWillLoadWindow: Event.None, ++ onWillShutdown: Event.None, ++ } as unknown as ILifecycleMainService, ++ fileService, ++ uriIdentityService, ++ )); ++ +@@ -362,0 +382,2 @@ ++ ++ socketServer.registerChannel('storage', disposables.add(new StorageDatabaseChannel(logService, storageMainService))); --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts -@@ -411,7 +411,8 @@ export class WebClientServer { - folderUri: resolveWorkspaceURI(this._environmentService.args['default-folder']), - workspaceUri: resolveWorkspaceURI(this._environmentService.args['default-workspace']), - productConfiguration, +@@ -411 +411,2 @@ - callbackRoute: callbackRoute + callbackRoute: callbackRoute, + remoteStorageEnabled: this._environmentService.args['enable-remote-storage'] ? true : undefined - }; - - const cookies = cookie.parse(req.headers.cookie || ''); -Index: code-server/lib/vscode/src/vs/workbench/browser/web.api.ts -=================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/browser/web.api.ts +++ code-server/lib/vscode/src/vs/workbench/browser/web.api.ts -@@ -411,6 +411,19 @@ export interface IWorkbenchConstructionO - - //#endregion - -+ -+ //#region Storage options -+ +@@ -413,0 +414,5 @@ + /** -+ * When true and connected to a remote authority, persist storage -+ * (settings, state, etc.) on the remote server instead of browser IndexedDB. -+ * This enables state portability across browsers/devices. -+ * @default false ++ * Persist workbench storage on the remote server instead of browser IndexedDB. + */ + readonly remoteStorageEnabled?: boolean; + -+ //#endregion -+ - } - - -Index: code-server/lib/vscode/src/vs/workbench/browser/web.main.ts -=================================================================== --- code-server.orig/lib/vscode/src/vs/workbench/browser/web.main.ts +++ code-server/lib/vscode/src/vs/workbench/browser/web.main.ts -@@ -33,7 +33,7 @@ import { ConfigurationCache } from '../s - import { ISignService } from '../../platform/sign/common/sign.js'; - import { SignService } from '../../platform/sign/browser/signService.js'; - import { IWorkbenchConstructionOptions, IWorkbench, IWorkspace, ITunnel } from './web.api.js'; --import { BrowserStorageService } from '../services/storage/browser/storageService.js'; -+import { BrowserStorageService, RemoteBrowserStorageService } from '../services/storage/browser/storageService.js'; - import { IStorageService } from '../../platform/storage/common/storage.js'; - import { toLocalISOString } from '../../base/common/date.js'; - import { isWorkspaceToOpen, isFolderToOpen } from '../../platform/window/common/window.js'; -@@ -475,1 +475,1 @@ +@@ -35,0 +36 @@ ++import { NativeWorkbenchStorageService } from '../services/storage/electron-browser/storageService.js'; +@@ -479 +480 @@ - this.createStorageService(workspace, logService, userDataProfileService).then(service => { -+ this.createStorageService(workspace, logService, userDataProfileService, remoteAgentService).then(service => { -@@ -597,7 +597,28 @@ - })); - } - ++ this.createStorageService(workspace, environmentService, logService, userDataProfileService, userDataProfilesService, remoteAgentService).then(service => { +@@ -601 +602,15 @@ - protected async createStorageService(workspace: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService): Promise { -+ protected async createStorageService(workspace: IAnyWorkspaceIdentifier, logService: ILogService, userDataProfileService: IUserDataProfileService, remoteAgentService: IRemoteAgentService): Promise { -+ // Use remote storage if enabled and connected to a remote server ++ protected async createStorageService(workspace: IAnyWorkspaceIdentifier, environmentService: IBrowserWorkbenchEnvironmentService, logService: ILogService, userDataProfileService: IUserDataProfileService, userDataProfilesService: IUserDataProfilesService, remoteAgentService: IRemoteAgentService): Promise { + const connection = remoteAgentService.getConnection(); + if (this.configuration.remoteStorageEnabled && connection) { -+ logService.info('Using remote storage service for browser state persistence'); -+ const storageService = new RemoteBrowserStorageService(workspace, userDataProfileService, connection, logService); ++ const storageService = new NativeWorkbenchStorageService(workspace, userDataProfileService, userDataProfilesService, connection, environmentService); + + try { + await storageService.initialize(); -+ -+ // Register to close on shutdown + this.onWillShutdownDisposables.add(toDisposable(() => storageService.close())); -+ + return storageService; + } catch (error) { + onUnexpectedError(error); + logService.error('Remote storage service initialization failed, falling back to browser storage', error); -+ // Fall through to browser storage on failure + } + } + -+ // Default: Use browser IndexedDB storage - const storageService = new BrowserStorageService(workspace, userDataProfileService, logService); - - try { -Index: code-server/lib/vscode/src/vs/sessions/browser/web.main.ts -=================================================================== --- code-server.orig/lib/vscode/src/vs/sessions/browser/web.main.ts +++ code-server/lib/vscode/src/vs/sessions/browser/web.main.ts -@@ -85,1 +85,1 @@ +@@ -42 +42 @@ +- _userDataProfilesService: BrowserUserDataProfilesService, ++ userDataProfilesService: BrowserUserDataProfilesService, +@@ -44 +44 @@ +- _remoteAgentService: IRemoteAgentService, ++ remoteAgentService: IRemoteAgentService, +@@ -85 +85 @@ - const storageService = await this.createStorageService(workspaceIdentifier, logService, userDataProfileService); -+ const storageService = await this.createStorageService(workspaceIdentifier, logService, userDataProfileService, _remoteAgentService); - -Index: code-server/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts -=================================================================== ---- code-server.orig/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts -+++ code-server/lib/vscode/src/vs/workbench/services/storage/browser/storageService.ts -@@ -21,3 +21,5 @@ - import { IUserDataProfileService } from '../../userDataProfile/common/userDataProfile.js'; -+import { ApplicationStorageDatabaseClient, ProfileStorageDatabaseClient, WorkspaceStorageDatabaseClient } from '../../../../platform/storage/common/storageIpc.js'; -+import { IRemoteAgentConnection } from '../../remote/common/remoteAgentService.js'; - - export class BrowserStorageService extends AbstractStorageService { -@@ -460,3 +462,188 @@ export class IndexedDBStorageDatabase ex - await db.runInTransaction(IndexedDBStorageDatabase.STORAGE_OBJECT_STORE, 'readwrite', objectStore => objectStore.clear()); - } - } -+ -+/** -+ * Storage service that uses the remote server for persistence via IPC, -+ * instead of browser IndexedDB. Enabled via the `remoteStorageEnabled` option. -+ */ -+export class RemoteBrowserStorageService extends AbstractStorageService { -+ -+ // private readonly applicationStorageProfile: IUserDataProfile; -+ -+ private readonly applicationStorage: IStorage; -+ -+ private profileStorageProfile: IUserDataProfile; -+ private readonly profileStorageDisposables = this._register(new DisposableStore()); -+ private profileStorage: IStorage; -+ -+ private workspaceStorageId: string | undefined; -+ private readonly workspaceStorageDisposables = this._register(new DisposableStore()); -+ private workspaceStorage: IStorage | undefined; -+ -+ constructor( -+ private readonly workspace: IAnyWorkspaceIdentifier, -+ private readonly userDataProfileService: IUserDataProfileService, -+ private readonly connection: IRemoteAgentConnection, -+ @ILogService private readonly logService: ILogService, -+ ) { -+ super(); -+ -+ -+ this.applicationStorage = this.createApplicationStorage(); -+ -+ this.profileStorageProfile = this.userDataProfileService.currentProfile; -+ this.profileStorage = this.createProfileStorage(this.profileStorageProfile); -+ -+ this.workspaceStorageId = this.workspace.id; -+ this.workspaceStorage = this.createWorkspaceStorage(this.workspace); -+ -+ this.registerListeners(); -+ } -+ -+ private registerListeners(): void { -+ this._register(this.userDataProfileService.onDidChangeCurrentProfile(e => e.join(this.switchToProfile(e.profile)))); -+ } -+ -+ private createApplicationStorage(): IStorage { -+ const storageDataBaseClient = this._register(new ApplicationStorageDatabaseClient(this.connection.getChannel('storage'))); -+ const applicationStorage = this._register(new Storage(storageDataBaseClient)); -+ -+ this._register(applicationStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.APPLICATION, e))); -+ -+ return applicationStorage; -+ } -+ -+ private createProfileStorage(profile: IUserDataProfile): IStorage { -+ -+ // First clear any previously associated disposables -+ this.profileStorageDisposables.clear(); -+ -+ // Remember profile associated to profile storage -+ this.profileStorageProfile = profile; -+ -+ let profileStorage: IStorage; -+ if (isProfileUsingDefaultStorage(profile)) { -+ -+ // If we are using default profile storage, the profile storage is -+ // actually the same as application storage. As such we -+ // avoid creating the storage library a second time on -+ // the same DB. -+ -+ profileStorage = this.applicationStorage; -+ } else { -+ const storageDataBaseClient = this.profileStorageDisposables.add(new ProfileStorageDatabaseClient(this.connection.getChannel('storage'), profile)); -+ profileStorage = this.profileStorageDisposables.add(new Storage(storageDataBaseClient)); -+ } -+ -+ this.profileStorageDisposables.add(profileStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.PROFILE, e))); -+ -+ return profileStorage; -+ } -+ -+ private createWorkspaceStorage(workspace: IAnyWorkspaceIdentifier): IStorage | undefined { -+ -+ // First clear any previously associated disposables -+ this.workspaceStorageDisposables.clear(); -+ -+ // Remember workspace ID for logging later -+ this.workspaceStorageId = workspace?.id; -+ -+ let workspaceStorage: IStorage | undefined = undefined; -+ if (workspace) { -+ const storageDataBaseClient = this.workspaceStorageDisposables.add(new WorkspaceStorageDatabaseClient(this.connection.getChannel('storage'), workspace)); -+ workspaceStorage = this.workspaceStorageDisposables.add(new Storage(storageDataBaseClient)); -+ -+ this.workspaceStorageDisposables.add(workspaceStorage.onDidChangeStorage(e => this.emitDidChangeValue(StorageScope.WORKSPACE, e))); -+ } -+ -+ return workspaceStorage; -+ } -+ -+ protected async doInitialize(): Promise { -+ -+ // Init all storage locations -+ await Promises.settled([ -+ this.applicationStorage.init(), -+ this.profileStorage.init(), -+ this.workspaceStorage?.init() ?? Promise.resolve() -+ ]); -+ } -+ -+ protected getStorage(scope: StorageScope): IStorage | undefined { -+ switch (scope) { -+ case StorageScope.APPLICATION: -+ return this.applicationStorage; -+ case StorageScope.PROFILE: -+ return this.profileStorage; -+ default: -+ return this.workspaceStorage; -+ } -+ } -+ -+ protected getLogDetails(scope: StorageScope): string | undefined { -+ switch (scope) { -+ case StorageScope.APPLICATION: -+ return 'Remote Application Storage'; -+ case StorageScope.PROFILE: -+ return `Remote Profile Storage (${this.profileStorageProfile.name})`; -+ default: -+ return this.workspaceStorageId ? `Remote Workspace Storage (${this.workspaceStorageId})` : undefined; -+ } -+ } -+ -+ protected async switchToProfile(toProfile: IUserDataProfile): Promise { -+ if (!this.canSwitchProfile(this.profileStorageProfile, toProfile)) { -+ return; -+ } -+ -+ const oldProfileStorage = this.profileStorage; -+ const oldItems = oldProfileStorage.items; -+ -+ // Close old profile storage but only if this is -+ // different from application storage! -+ if (oldProfileStorage !== this.applicationStorage) { -+ await oldProfileStorage.close(); -+ } -+ -+ // Create new profile storage & init -+ this.profileStorage = this.createProfileStorage(toProfile); -+ await this.profileStorage.init(); -+ -+ // Handle data switch and eventing -+ this.switchData(oldItems, this.profileStorage, StorageScope.PROFILE); -+ } -+ -+ protected async switchToWorkspace(toWorkspace: IAnyWorkspaceIdentifier, preserveData: boolean): Promise { -+ const oldWorkspaceStorage = this.workspaceStorage; -+ const oldItems = oldWorkspaceStorage?.items ?? new Map(); -+ -+ // Close old workspace storage -+ await oldWorkspaceStorage?.close(); -+ -+ // Create new workspace storage & init -+ this.workspaceStorage = this.createWorkspaceStorage(toWorkspace); -+ await this.workspaceStorage?.init(); -+ -+ // Handle data switch and eventing -+ if (this.workspaceStorage) { -+ this.switchData(oldItems, this.workspaceStorage, StorageScope.WORKSPACE); -+ } -+ } -+ -+ hasScope(scope: IAnyWorkspaceIdentifier | IUserDataProfile): boolean { -+ if (isUserDataProfile(scope)) { -+ return this.profileStorageProfile.id === scope.id; -+ } -+ -+ return this.workspaceStorageId === scope.id; -+ } -+ -+ close(): void { -+ this.logService.trace('RemoteBrowserStorageService#close()'); -+ -+ // Always dispose to ensure that no timeouts or callbacks -+ // get triggered in this phase. -+ this.dispose(); -+ } -+} -Index: code-server/src/node/cli.ts -=================================================================== ---- code-server.orig/src/node/cli.ts -+++ code-server/src/node/cli.ts -@@ -56,6 +56,7 @@ export interface UserProvidedCodeArgs { - "session-socket"?: string - "cookie-suffix"?: string - "link-protection-trusted-domains"?: string[] -+ "enable-remote-storage"?: boolean - // locale is used by both VS Code and code-server. - locale?: string - } -@@ -303,6 +304,11 @@ export const options: Options Date: Tue, 8 Sep 2026 07:30:49 +0000 Subject: [PATCH 03/12] fix: refresh remote storage channel patch --- patches/remote-storage.diff | 9 +++++++-- 1 file changed, 7 insertions(+), 2 deletions(-) diff --git a/patches/remote-storage.diff b/patches/remote-storage.diff index f4804fd64f5b..c8e94ce3e714 100644 --- a/patches/remote-storage.diff +++ b/patches/remote-storage.diff @@ -40,9 +40,14 @@ Reuse VS Code's Electron storage service for remote persistence + uriIdentityService, + )); + -@@ -362,0 +382,2 @@ -+ +@@ -419,6 +439,7 @@ export async function setupServerServices(connectionToken: ServerConnectionToken + + const languagePackChannel = ProxyChannel.fromService(accessor.get(ILanguagePackService), disposables); + socketServer.registerChannel('languagePacks', languagePackChannel); + socketServer.registerChannel('storage', disposables.add(new StorageDatabaseChannel(logService, storageMainService))); + + // clean up extensions folder + remoteExtensionsScanner.whenExtensionsReady().then(() => extensionManagementService.cleanUp()); --- code-server.orig/lib/vscode/src/vs/server/node/webClientServer.ts +++ code-server/lib/vscode/src/vs/server/node/webClientServer.ts @@ -411 +411,2 @@ From 371897c4f80ca87dd094628edb7c7179e2e97815 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:44:59 +0000 Subject: [PATCH 04/12] chore: keep cache key change out of storage refactor --- .github/workflows/build.yaml | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 93978f6dfd38..62424f13e196 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -194,7 +194,7 @@ jobs: uses: actions/cache@55cc8345863c7cc4c66a329aec7e433d2d1c52a9 # v6.1.0 with: path: lib/vscode-reh-web-linux-x64 - key: vscode-linux-x64-package-${{ secrets.VSCODE_CACHE_VERSION }}-${{ steps.vscode-rev.outputs.rev }}-${{ hashFiles('patches/*.diff', 'patches/series', 'ci/build/build-vscode.sh') }} + key: vscode-linux-x64-package-${{ secrets.VSCODE_CACHE_VERSION }}-${{ steps.vscode-rev.outputs.rev }}-${{ hashFiles('patches/*.diff', 'ci/build/build-vscode.sh') }} - name: Build vscode if: steps.cache-vscode.outputs.cache-hit != 'true' run: | From ba62df839f468703dd313f63feabdccea618e438 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 07:51:08 +0000 Subject: [PATCH 05/12] ci: allow manual build workflow dispatch --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 62424f13e196..1d1531df0f88 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,6 +1,7 @@ name: Build on: + workflow_dispatch: push: branches: - main From 3c0bf9da5eb40d02cb12c90b069c45f52ce1d082 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:14:13 +0000 Subject: [PATCH 06/12] feat: expose remote storage flag in code-server cli --- src/node/cli.ts | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/node/cli.ts b/src/node/cli.ts index 926f6ff1fdef..b631a4319e5f 100644 --- a/src/node/cli.ts +++ b/src/node/cli.ts @@ -46,6 +46,7 @@ export interface UserProvidedCodeArgs { "show-versions"?: boolean category?: string "github-auth"?: string + "enable-remote-storage"?: boolean "disable-update-check"?: boolean "disable-file-downloads"?: boolean "disable-file-uploads"?: boolean @@ -274,6 +275,10 @@ export const options: Options> = { type: "string", description: "GitHub authentication token (can only be passed in via $GITHUB_TOKEN or the config file).", }, + "enable-remote-storage": { + type: "boolean", + description: "Persist VS Code workbench storage on the remote server instead of browser storage.", + }, "proxy-domain": { type: "string[]", description: "Domain used for proxying ports." }, "skip-auth-preflight": { type: "boolean", From 8fdadea880299e92a63ba23467fe8b7a864cffa2 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:15:12 +0000 Subject: [PATCH 07/12] test: cover remote storage cli flag --- test/unit/node/cli.test.ts | 7 +++++++ 1 file changed, 7 insertions(+) diff --git a/test/unit/node/cli.test.ts b/test/unit/node/cli.test.ts index 53eb066c1e70..3e50a222ae8f 100644 --- a/test/unit/node/cli.test.ts +++ b/test/unit/node/cli.test.ts @@ -1036,6 +1036,13 @@ describe("toCodeArgs", () => { }) }) + it("should pass through --enable-remote-storage", async () => { + expect(await toCodeArgs(await setDefaults(parse(["--enable-remote-storage"])))).toStrictEqual({ + ...vscodeDefaults, + "enable-remote-storage": true, + }) + }) + it("should collect a repeated --vscode-option into an array", async () => { const args = parse([ "--vscode-option", From 8c74f4d4dd4066f3c292881f4a82c5ea36a6c6e9 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 08:43:53 +0000 Subject: [PATCH 08/12] ci: remove manual build trigger --- .github/workflows/build.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 1d1531df0f88..62424f13e196 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -1,7 +1,6 @@ name: Build on: - workflow_dispatch: push: branches: - main From ce5016ae86ab7208865765ad7c98a760c58ce46a Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:02:40 +0000 Subject: [PATCH 09/12] fix: avoid loading electron in remote storage server --- patches/remote-storage.diff | 18 ++++++++++++++++++ 1 file changed, 18 insertions(+) diff --git a/patches/remote-storage.diff b/patches/remote-storage.diff index c8e94ce3e714..fad692190cb1 100644 --- a/patches/remote-storage.diff +++ b/patches/remote-storage.diff @@ -1,5 +1,23 @@ Reuse VS Code's Electron storage service for remote persistence +--- code-server.orig/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts ++++ code-server/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts +@@ -14 +14 @@ +-import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; ++import type { ILifecycleMainService, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; +@@ -101 +101 @@ +- @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, ++ private readonly lifecycleMainService: ILifecycleMainService, +@@ -117 +117 @@ +- await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); ++ await this.lifecycleMainService.when(3 /* LifecycleMainPhase.AfterWindowOpen */); +@@ -256 +256 @@ +- if (this.shutdownReason === ShutdownReason.KILL) { ++ if (this.shutdownReason === 2 /* ShutdownReason.KILL */) { +@@ -295 +295 @@ +- if (this.shutdownReason === ShutdownReason.KILL) { ++ if (this.shutdownReason === 2 /* ShutdownReason.KILL */) { + --- code-server.orig/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts +++ code-server/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts @@ -6 +6 @@ From 90eb129fc93ae79cdba766322e1b541da3e39866 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 12:50:50 +0000 Subject: [PATCH 10/12] refactor: name server storage lifecycle constants --- patches/remote-storage.diff | 11 ++++++++--- 1 file changed, 8 insertions(+), 3 deletions(-) diff --git a/patches/remote-storage.diff b/patches/remote-storage.diff index fad692190cb1..dca9228f55f6 100644 --- a/patches/remote-storage.diff +++ b/patches/remote-storage.diff @@ -5,18 +5,23 @@ Reuse VS Code's Electron storage service for remote persistence @@ -14 +14 @@ -import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; +import type { ILifecycleMainService, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; ++ ++// Keep these values local so the server does not load lifecycleMainService at runtime; ++// that module imports Electron, which is not present in the code-server server image. ++const LIFECYCLE_MAIN_PHASE_AFTER_WINDOW_OPEN = 3; ++const SHUTDOWN_REASON_KILL = 2; @@ -101 +101 @@ - @ILifecycleMainService private readonly lifecycleMainService: ILifecycleMainService, + private readonly lifecycleMainService: ILifecycleMainService, @@ -117 +117 @@ - await this.lifecycleMainService.when(LifecycleMainPhase.AfterWindowOpen); -+ await this.lifecycleMainService.when(3 /* LifecycleMainPhase.AfterWindowOpen */); ++ await this.lifecycleMainService.when(LIFECYCLE_MAIN_PHASE_AFTER_WINDOW_OPEN); @@ -256 +256 @@ - if (this.shutdownReason === ShutdownReason.KILL) { -+ if (this.shutdownReason === 2 /* ShutdownReason.KILL */) { ++ if (this.shutdownReason === SHUTDOWN_REASON_KILL) { @@ -295 +295 @@ - if (this.shutdownReason === ShutdownReason.KILL) { -+ if (this.shutdownReason === 2 /* ShutdownReason.KILL */) { ++ if (this.shutdownReason === SHUTDOWN_REASON_KILL) { --- code-server.orig/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts +++ code-server/lib/vscode/src/vs/workbench/services/storage/electron-browser/storageService.ts From ae88c1a745c25b684d9a80503c96de17a93450d0 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 13:52:32 +0000 Subject: [PATCH 11/12] fix: render helm chart before validation --- .github/workflows/build.yaml | 5 ++++- 1 file changed, 4 insertions(+), 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index 62424f13e196..be6d94e73ce2 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -95,7 +95,10 @@ jobs: - uses: bmuschko/setup-kubeconform@e5d264ec1eafe2314e750c429a9da7639dd5a003 # v1.1.0 with: kubeconform-version: "0.8.0" - - run: kubeconform ci/helm-chart + - uses: azure/setup-helm@v4 + with: + version: v3.17.3 + - run: helm template code-server ci/helm-chart | kubeconform -strict -summary - lint-ts: name: Lint TypeScript files From 48bd5e1107eaf0fedc805ee2104ce4825f701fd7 Mon Sep 17 00:00:00 2001 From: Daywin67 <57551491+Daywin67@users.noreply.github.com> Date: Tue, 8 Sep 2026 14:12:37 +0000 Subject: [PATCH 12/12] fix: make lifecycle constants part of quilt patch --- patches/remote-storage.diff | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/patches/remote-storage.diff b/patches/remote-storage.diff index dca9228f55f6..b38f89e39cb9 100644 --- a/patches/remote-storage.diff +++ b/patches/remote-storage.diff @@ -2,7 +2,7 @@ Reuse VS Code's Electron storage service for remote persistence --- code-server.orig/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts +++ code-server/lib/vscode/src/vs/platform/storage/electron-main/storageMainService.ts -@@ -14 +14 @@ +@@ -14 +14,6 @@ -import { ILifecycleMainService, LifecycleMainPhase, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; +import type { ILifecycleMainService, ShutdownReason } from '../../lifecycle/electron-main/lifecycleMainService.js'; +