Implement OS color scheme auto-detection for new users (#302761)

* Implement auto-detect color scheme feature for new users and update related services

* Add OS color-scheme auto-detection support for new users

Co-authored-by: Copilot <copilot@github.com>

* Add assertions to verify color scheme detection for high contrast themes

Co-authored-by: Copilot <copilot@github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

* Potential fix for pull request finding

Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>

---------

Co-authored-by: mrleemurray <mrleemurray@users.noreply.github.com>
Co-authored-by: Copilot <copilot@github.com>
Co-authored-by: Copilot Autofix powered by AI <175728472+Copilot@users.noreply.github.com>
This commit is contained in:
Lee Murray
2026-03-18 12:57:31 +00:00
committed by GitHub
parent aeb06c306d
commit 331c3ba9c9
6 changed files with 191 additions and 7 deletions

View File

@@ -23,4 +23,12 @@ export interface IThemeMainService {
getWindowSplash(workspace: IWorkspaceIdentifier | ISingleFolderWorkspaceIdentifier | undefined): IPartsSplash | undefined;
getColorScheme(): IColorScheme;
/**
* Whether OS color-scheme auto-detection is active.
* Returns `true` when the `window.autoDetectColorScheme` setting is enabled,
* or for fresh installs where no theme has been stored yet and the user
* has not explicitly configured the setting (e.g. via settings sync).
*/
isAutoDetectColorScheme(): boolean;
}

View File

@@ -113,7 +113,7 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
}
private updateSystemColorTheme(): void {
if (isLinux || Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) {
if (isLinux || this.isAutoDetectColorScheme()) {
electron.nativeTheme.themeSource = 'system'; // only with `system` we can detect the system color scheme
} else {
switch (Setting.SYSTEM_COLOR_THEME.getValue(this.configurationService)) {
@@ -174,13 +174,26 @@ export class ThemeMainService extends Disposable implements IThemeMainService {
return colorScheme.dark ? ThemeTypeSelector.HC_BLACK : ThemeTypeSelector.HC_LIGHT;
}
if (Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) {
if (this.isAutoDetectColorScheme()) {
return colorScheme.dark ? ThemeTypeSelector.VS_DARK : ThemeTypeSelector.VS;
}
return undefined;
}
isAutoDetectColorScheme(): boolean {
if (Setting.DETECT_COLOR_SCHEME.getValue(this.configurationService)) {
return true;
}
// For new installs with no stored theme, auto-detect OS color scheme by default
// unless the user has explicitly configured the setting (e.g. via settings sync)
if (!this.stateService.getItem(THEME_STORAGE_KEY)) {
const { userValue } = this.configurationService.inspect<boolean>(Setting.DETECT_COLOR_SCHEME.key);
return userValue === undefined;
}
return false;
}
getBackgroundColor(): string {
const preferred = this.getPreferredBaseTheme();
const stored = this.getStoredBaseTheme();

View File

@@ -1561,7 +1561,7 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic
os: { release: release(), hostname: hostname(), arch: arch() },
autoDetectHighContrast: windowConfig?.autoDetectHighContrast ?? true,
autoDetectColorScheme: windowConfig?.autoDetectColorScheme ?? false,
autoDetectColorScheme: windowConfig?.autoDetectColorScheme ?? this.themeMainService.isAutoDetectColorScheme(),
accessibilitySupport: app.accessibilitySupportEnabled,
colorScheme: this.themeMainService.getColorScheme(),
policiesData: this.policyService.serialize(),

View File

@@ -118,7 +118,8 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme
) {
super();
this.container = layoutService.mainContainer;
this.settings = new ThemeConfiguration(configurationService, hostColorService);
const isNewUser = this.storageService.isNew(StorageScope.APPLICATION);
this.settings = new ThemeConfiguration(configurationService, hostColorService, isNewUser);
this.colorThemeRegistry = this._register(new ThemeRegistry(colorThemesExtPoint, ColorThemeData.fromExtensionTheme));
this.colorThemeWatcher = this._register(new ThemeFileWatcher(fileService, environmentService, this.reloadCurrentColorTheme.bind(this)));
@@ -245,6 +246,7 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme
this.migrateColorThemeSettings();
await this.migrateAutoDetectColorScheme();
const result = await Promise.all([initializeColorTheme(), initializeFileIconTheme(), initializeProductIconTheme()]);
this.showNewDefaultThemeNotification();
return result;
@@ -303,6 +305,29 @@ export class WorkbenchThemeService extends Disposable implements IWorkbenchTheme
}
}
/**
* For new users who haven't explicitly configured `window.autoDetectColorScheme`,
* persist `true` so that auto-detect becomes the default going forward.
*/
private async migrateAutoDetectColorScheme(): Promise<void> {
if (!this.storageService.isNew(StorageScope.APPLICATION)) {
return;
}
// Ensure that user data (including synced settings) has finished initializing
// so we do not overwrite values that arrive via settings sync.
await this.userDataInitializationService.whenInitializationFinished();
const inspection = this.configurationService.inspect<boolean>(ThemeSettings.DETECT_COLOR_SCHEME);
// Treat any of userValue, userLocalValue, or userRemoteValue as an explicit configuration.
if (inspection.userValue === undefined
&& inspection.userLocalValue === undefined
&& inspection.userRemoteValue === undefined) {
await this.configurationService.updateValue(ThemeSettings.DETECT_COLOR_SCHEME, true, ConfigurationTarget.USER);
}
}
private installConfigurationListener() {
this._register(this.configurationService.onDidChangeConfiguration(e => {
if (e.affectsConfiguration(ThemeSettings.COLOR_THEME)

View File

@@ -282,7 +282,19 @@ const colorSchemeToPreferred = {
};
export class ThemeConfiguration {
constructor(private configurationService: IConfigurationService, private hostColorService: IHostColorSchemeService) {
constructor(private configurationService: IConfigurationService, private hostColorService: IHostColorSchemeService, private readonly isNewUser: boolean = false) {
}
private shouldAutoDetectColorScheme(): boolean {
const { value, userValue, userLocalValue, userRemoteValue } = this.configurationService.inspect<boolean>(ThemeSettings.DETECT_COLOR_SCHEME);
if (value) {
return true;
}
if (this.isNewUser) {
const hasUserScopedValue = userValue !== undefined || userLocalValue !== undefined || userRemoteValue !== undefined;
return !hasUserScopedValue;
}
return false;
}
public get colorTheme(): string {
@@ -336,14 +348,14 @@ export class ThemeConfiguration {
if (this.configurationService.getValue(ThemeSettings.DETECT_HC) && this.hostColorService.highContrast) {
return this.hostColorService.dark ? ColorScheme.HIGH_CONTRAST_DARK : ColorScheme.HIGH_CONTRAST_LIGHT;
}
if (this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME)) {
if (this.shouldAutoDetectColorScheme()) {
return this.hostColorService.dark ? ColorScheme.DARK : ColorScheme.LIGHT;
}
return undefined;
}
public isDetectingColorScheme(): boolean {
return this.configurationService.getValue(ThemeSettings.DETECT_COLOR_SCHEME);
return this.shouldAutoDetectColorScheme();
}
public getColorThemeSettingId(): ThemeSettings {

View File

@@ -6,6 +6,12 @@
import assert from 'assert';
import { migrateThemeSettingsId } from '../../common/workbenchThemeService.js';
import { ensureNoDisposablesAreLeakedInTestSuite } from '../../../../../base/test/common/utils.js';
import { ThemeConfiguration } from '../../common/themeConfiguration.js';
import { TestConfigurationService } from '../../../../../platform/configuration/test/common/testConfigurationService.js';
import { IHostColorSchemeService } from '../../common/hostColorSchemeService.js';
import { ColorScheme } from '../../../../../platform/theme/common/theme.js';
import { Event } from '../../../../../base/common/event.js';
import { IConfigurationOverrides, IConfigurationValue } from '../../../../../platform/configuration/common/configuration.js';
suite('WorkbenchThemeService', () => {
@@ -34,4 +40,124 @@ suite('WorkbenchThemeService', () => {
);
});
});
suite('ThemeConfiguration', () => {
function createHostColorSchemeService(dark: boolean, highContrast: boolean = false): IHostColorSchemeService {
return {
_serviceBrand: undefined,
dark,
highContrast,
onDidChangeColorScheme: Event.None,
};
}
/**
* Creates a config service that separates the resolved value from the user value,
* matching production behaviour where getValue() returns the schema default
* while inspect().userValue is undefined when no explicit user value is set.
*/
function createConfigServiceWithDefaults(
userConfig: Record<string, unknown>,
defaults: Record<string, unknown>
): TestConfigurationService {
const configService = new TestConfigurationService(userConfig);
const originalInspect = configService.inspect.bind(configService);
configService.inspect = <T>(key: string, overrides?: IConfigurationOverrides): IConfigurationValue<T> => {
if (Object.prototype.hasOwnProperty.call(userConfig, key)) {
return originalInspect(key, overrides);
}
// No explicit user value: return the default as the resolved value
const defaultVal = defaults[key] as T;
return {
value: defaultVal,
defaultValue: defaultVal,
userValue: undefined,
userLocalValue: undefined,
};
};
const originalGetValue = configService.getValue.bind(configService);
configService.getValue = <T>(arg1?: string | IConfigurationOverrides, arg2?: IConfigurationOverrides): T => {
const result = originalGetValue(arg1, arg2);
if (result === undefined && typeof arg1 === 'string' && Object.prototype.hasOwnProperty.call(defaults, arg1)) {
return defaults[arg1] as T;
}
return result as T;
};
return configService;
}
test('new user with no explicit setting gets auto-detect on light OS', () => {
const configService = new TestConfigurationService();
const hostColor = createHostColorSchemeService(false);
const config = new ThemeConfiguration(configService, hostColor, true);
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT);
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
});
test('new user with no explicit setting gets auto-detect on dark OS', () => {
const configService = new TestConfigurationService();
const hostColor = createHostColorSchemeService(true);
const config = new ThemeConfiguration(configService, hostColor, true);
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.DARK);
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
});
test('new user with no explicit setting and schema default false still gets auto-detect', () => {
// Simulates production: getValue() returns false (schema default) but userValue is undefined
const configService = createConfigServiceWithDefaults({}, { 'window.autoDetectColorScheme': false });
const hostColor = createHostColorSchemeService(false);
const config = new ThemeConfiguration(configService, hostColor, true);
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT);
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
});
test('new user with explicit false does not get auto-detect', () => {
const configService = new TestConfigurationService({ 'window.autoDetectColorScheme': false });
const hostColor = createHostColorSchemeService(false);
const config = new ThemeConfiguration(configService, hostColor, true);
assert.deepStrictEqual(config.getPreferredColorScheme(), undefined);
assert.deepStrictEqual(config.isDetectingColorScheme(), false);
});
test('existing user without explicit setting does not get auto-detect', () => {
const configService = new TestConfigurationService();
const hostColor = createHostColorSchemeService(false);
const config = new ThemeConfiguration(configService, hostColor, false);
assert.deepStrictEqual(config.getPreferredColorScheme(), undefined);
assert.deepStrictEqual(config.isDetectingColorScheme(), false);
});
test('existing user with explicit true gets auto-detect', () => {
const configService = new TestConfigurationService({ 'window.autoDetectColorScheme': true });
const hostColor = createHostColorSchemeService(false);
const config = new ThemeConfiguration(configService, hostColor, false);
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.LIGHT);
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
});
test('high contrast OS takes priority over auto-detect for new user', () => {
const configService = new TestConfigurationService({ 'window.autoDetectHighContrast': true });
const hostColor = createHostColorSchemeService(true, true);
const config = new ThemeConfiguration(configService, hostColor, true);
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.HIGH_CONTRAST_DARK);
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
});
test('high contrast light OS takes priority over auto-detect for new user', () => {
const configService = new TestConfigurationService({ 'window.autoDetectHighContrast': true });
const hostColor = createHostColorSchemeService(false, true);
const config = new ThemeConfiguration(configService, hostColor, true);
assert.deepStrictEqual(config.getPreferredColorScheme(), ColorScheme.HIGH_CONTRAST_LIGHT);
assert.deepStrictEqual(config.isDetectingColorScheme(), true);
});
});
});