From 0c163820423721343e2f22f6acb2266a5aa04d71 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 29 Apr 2026 16:56:55 +0200 Subject: [PATCH 01/14] agents welcome: add theme selection step after sign-in After signing in (or for first-launch signed-in users after clicking Get Started), show a theme selection step with 4 theme preview cards (Dark 2026, Dark HC, Light 2026, Light HC). Theme is applied live on card click. Continue button dismisses the overlay. External sign-in via onDidChangeDefaultAccount now transitions to the theme step instead of auto-completing. Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../browser/media/sessionsWalkthrough.css | 103 +++++++++++++ .../welcome/browser/sessionsWalkthrough.ts | 136 +++++++++++++++++- .../welcome/browser/welcome.contribution.ts | 5 +- 3 files changed, 238 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index 3fc399f509bb3..2cf1409f0295a 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -423,6 +423,109 @@ margin-top: 4px; } +/* ---- Theme step ---- */ + +.sessions-walkthrough-theme-header { + text-align: center; + margin-bottom: 8px; +} + +.sessions-walkthrough-theme-header h2 { + margin: 0; + font-size: 22px; + font-weight: 600; + color: var(--vscode-foreground); +} + +.sessions-walkthrough-theme-header p { + margin: 6px 0 0; + font-size: 13px; + color: var(--vscode-descriptionForeground); + line-height: 1.5; +} + +.sessions-walkthrough-theme-grid { + display: grid; + grid-template-columns: repeat(4, minmax(0, 1fr)); + gap: 10px; + width: 100%; +} + +.sessions-walkthrough-theme-card { + border-radius: 8px; + border: 2px solid var(--vscode-widget-border, rgba(255, 255, 255, 0.1)); + cursor: pointer; + overflow: hidden; + transition: border-color 100ms, transform 100ms; +} + +.sessions-walkthrough-theme-card:hover { + border-color: var(--vscode-focusBorder, #007acc); + transform: translateY(-1px); +} + +.sessions-walkthrough-theme-card:focus-visible { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + +.sessions-walkthrough-theme-card.selected { + border-color: var(--vscode-focusBorder, #007acc); + box-shadow: 0 0 0 1px var(--vscode-focusBorder, #007acc); +} + +.monaco-workbench.hc-black .sessions-walkthrough-theme-card, +.monaco-workbench.hc-light .sessions-walkthrough-theme-card { + border-width: 2px; + border-color: var(--vscode-contrastBorder); +} + +.monaco-workbench.hc-black .sessions-walkthrough-theme-card:hover, +.monaco-workbench.hc-light .sessions-walkthrough-theme-card:hover { + border-color: var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); +} + +.monaco-workbench.hc-black .sessions-walkthrough-theme-card:focus-visible, +.monaco-workbench.hc-light .sessions-walkthrough-theme-card:focus-visible { + outline: 2px solid var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); + outline-offset: 2px; +} + +.monaco-workbench.hc-black .sessions-walkthrough-theme-card.selected, +.monaco-workbench.hc-light .sessions-walkthrough-theme-card.selected { + border-color: var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); + box-shadow: 0 0 0 1px var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); +} + +.sessions-walkthrough-theme-preview { + overflow: hidden; +} + +.sessions-walkthrough-theme-preview-img { + display: block; + width: 100%; + height: auto; +} + +.sessions-walkthrough-theme-label { + padding: 4px 6px; + font-size: 12px; + font-weight: 500; + text-align: center; + color: var(--vscode-foreground); +} + +.sessions-walkthrough-theme-footer { + display: flex; + justify-content: center; +} + +@media (max-width: 480px) { + .sessions-walkthrough-theme-grid { + grid-template-columns: repeat(2, minmax(0, 1fr)); + } +} + /* Reduced motion */ .monaco-reduce-motion .sessions-walkthrough-overlay, diff --git a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts index 4c134230f4b58..3323cfa994dd9 100644 --- a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts +++ b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts @@ -8,7 +8,10 @@ import { disposableTimeout } from '../../../../base/common/async.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { $, addDisposableGenericMouseDownListener, append, EventType, addDisposableListener, getActiveElement, isHTMLElement } from '../../../../base/browser/dom.js'; import { localize } from '../../../../nls.js'; +import { FileAccess } from '../../../../base/common/network.js'; +import { IProductOnboardingTheme } from '../../../../base/common/product.js'; import { ICommandService } from '../../../../platform/commands/common/commands.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IOpenerService } from '../../../../platform/opener/common/opener.js'; import { IProductService } from '../../../../platform/product/common/productService.js'; @@ -19,6 +22,7 @@ import { URI } from '../../../../base/common/uri.js'; import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../../../workbench/contrib/chat/browser/actions/chatActions.js'; import { ChatSetupStrategy } from '../../../../workbench/contrib/chat/browser/chatSetup/chatSetup.js'; import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; +import { IWorkbenchThemeService } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; export type WalkthroughOutcome = 'completed' | 'dismissed'; @@ -69,6 +73,17 @@ export class SessionsWalkthroughOverlay extends Disposable { */ get isShowingSignIn(): boolean { return this._isShowingSignIn; } + /** + * Transition to the theme selection step. Called by external code + * (e.g. the contribution) when the user signs in while the sign-in + * screen is visible, so the user still gets to pick a theme before + * the overlay dismisses. + */ + showThemeStep(): void { + this._isShowingSignIn = false; + this._renderThemeStep(); + } + /** Resolves when the user completes or dismisses the walkthrough. */ readonly outcome: Promise = new Promise(resolve => { this._resolveOutcome = resolve; }); @@ -81,6 +96,7 @@ export class SessionsWalkthroughOverlay extends Disposable { @IExtensionService private readonly extensionService: IExtensionService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, + @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @ILogService private readonly logService: ILogService, ) { super(); @@ -291,7 +307,7 @@ export class SessionsWalkthroughOverlay extends Disposable { getStartedBtn.textContent = localize('walkthrough.welcome.getStarted', "Get Started"); stepDisposables.add(addDisposableListener(getStartedBtn, EventType.CLICK, () => { this._isShowingWelcome = false; - this.complete(); + this._renderThemeStep(); })); this.currentFocusableElements = [getStartedBtn, ...this.disclaimerLinks]; @@ -307,6 +323,120 @@ export class SessionsWalkthroughOverlay extends Disposable { return this.defaultAccountService.currentDefaultAccount !== null; } + // ------------------------------------------------------------------ + // Theme Step + + private _renderThemeStep(): void { + const stepDisposables = this.stepDisposables.value = new DisposableStore(); + this._isShowingWelcome = true; + + // Fade out current content, then render theme step + this.contentContainer.classList.add('sessions-walkthrough-fade-out'); + stepDisposables.add(disposableTimeout(() => { + if (!this.overlay.isConnected) { + return; + } + this.contentContainer.classList.remove('sessions-walkthrough-fade-out'); + this._renderThemeStepContent(stepDisposables); + }, fadeDuration)); + } + + private _renderThemeStepContent(stepDisposables: DisposableStore): void { + this.contentContainer.textContent = ''; + this.footerContainer.textContent = ''; + this.disclaimerElement.classList.add('hidden'); + + // Header + const header = append(this.contentContainer, $('.sessions-walkthrough-theme-header')); + append(header, $('h2', undefined, localize('walkthrough.theme.title', "Choose Your Theme"))); + append(header, $('p', undefined, localize('walkthrough.theme.subtitle', "Pick a color theme to make it yours. You can always change it later."))); + + // Theme grid — exclude solarized variants, show 4 cards + const allThemes = this.productService.onboardingThemes ?? []; + const themes = allThemes.filter(t => !t.id.startsWith('solarized')); + + const themeGrid = append(this.contentContainer, $('.sessions-walkthrough-theme-grid')); + themeGrid.setAttribute('role', 'radiogroup'); + themeGrid.setAttribute('aria-label', localize('walkthrough.theme.ariaLabel', "Choose a color theme")); + + // Detect current theme to pre-select + const currentTheme = this.themeService.getColorTheme(); + let selectedThemeId = themes.find(t => t.themeId === currentTheme.settingsId)?.id ?? themes[0]?.id; + + const themeCards: HTMLElement[] = []; + for (const theme of themes) { + const card = this._createThemeCard(stepDisposables, themeGrid, theme, themeCards, selectedThemeId, id => { selectedThemeId = id; }); + themeCards.push(card); + } + + // Footer with Continue button + const actions = append(this.footerContainer, $('.sessions-walkthrough-theme-footer')); + const continueBtn = append(actions, $('button.sessions-walkthrough-get-started-btn')) as HTMLButtonElement; + continueBtn.textContent = localize('walkthrough.theme.continue', "Continue"); + stepDisposables.add(addDisposableListener(continueBtn, EventType.CLICK, () => { + this._isShowingWelcome = false; + this.complete(); + })); + + this.currentFocusableElements = [...themeCards, continueBtn]; + + stepDisposables.add(disposableTimeout(() => { + if (this.overlay.isConnected) { + continueBtn.focus(); + } + }, 0)); + } + + private _createThemeCard(stepDisposables: DisposableStore, parent: HTMLElement, theme: IProductOnboardingTheme, allCards: HTMLElement[], selectedThemeId: string, onSelect: (id: string) => void): HTMLElement { + const card = append(parent, $('div.sessions-walkthrough-theme-card')); + card.setAttribute('role', 'radio'); + card.setAttribute('aria-checked', theme.id === selectedThemeId ? 'true' : 'false'); + card.setAttribute('aria-label', theme.label); + card.setAttribute('tabindex', '0'); + + if (theme.id === selectedThemeId) { + card.classList.add('selected'); + } + + // SVG preview image + const preview = append(card, $('div.sessions-walkthrough-theme-preview')); + const img = append(preview, $('img.sessions-walkthrough-theme-preview-img')); + img.alt = ''; + img.src = FileAccess.asBrowserUri(`vs/workbench/contrib/welcomeOnboarding/browser/media/theme-preview-${theme.id}.svg`).toString(true); + + // Label + const label = append(card, $('div.sessions-walkthrough-theme-label')); + label.textContent = theme.label; + + stepDisposables.add(addDisposableListener(card, EventType.CLICK, () => { + onSelect(theme.id); + this._applyTheme(theme); + for (const c of allCards) { + c.classList.remove('selected'); + c.setAttribute('aria-checked', 'false'); + } + card.classList.add('selected'); + card.setAttribute('aria-checked', 'true'); + })); + + stepDisposables.add(addDisposableListener(card, EventType.KEY_DOWN, (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + card.click(); + } + })); + + return card; + } + + private async _applyTheme(theme: IProductOnboardingTheme): Promise { + const allThemes = await this.themeService.getColorThemes(); + const match = allThemes.find(t => t.settingsId === theme.themeId); + if (match) { + this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); + } + } + private async _runSignIn(providerButtons: HTMLButtonElement[], error: HTMLElement, strategy: ChatSetupStrategy, titleEl: HTMLElement, subtitleEl: HTMLElement, signInActions: HTMLElement): Promise { await this._fadeToProgress(providerButtons, error, titleEl, subtitleEl, signInActions); if (this._shouldAbortUpdate(titleEl, subtitleEl)) { @@ -339,7 +469,7 @@ export class SessionsWalkthroughOverlay extends Disposable { return; } } - this.complete(); + this._renderThemeStep(); } else { await this._showErrorAndReset(error, localize('walkthrough.canceledError', "Sign-in was canceled. Please try again.")); } @@ -365,7 +495,7 @@ export class SessionsWalkthroughOverlay extends Disposable { const scopes = this.productService.defaultChatAgent?.providerScopes?.[0] ?? ['read:user', 'user:email', 'repo', 'workflow']; await this.authenticationService.createSession('github', scopes, { activateImmediate: true }); - this.complete(); + this._renderThemeStep(); } catch (err) { this.logService.error('[sessions walkthrough] Web sign-in failed:', err); await this._showErrorAndReset(error, localize('walkthrough.signInError', "Something went wrong. Please try again.")); diff --git a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts index 0d0d9b2b5bc93..2c533a94fc838 100644 --- a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts +++ b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts @@ -66,8 +66,7 @@ export function resetSessionsWelcome( store.add(defaultAccountService.onDidChangeDefaultAccount(account => { if (!walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) { storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - walkthrough.complete(); - store.dispose(); + walkthrough.showThemeStep(); } })); @@ -240,7 +239,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc if (!welcomeCompletionStored && !walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) { welcomeCompletionStored = true; this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - walkthrough.complete(); + walkthrough.showThemeStep(); } })); From ffbe806d892edfecbd9993dee82237c8a276b862 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Wed, 29 Apr 2026 23:35:54 +0200 Subject: [PATCH 02/14] agents welcome: theme importer restructure and environment improvements - Rename vsCodeThemeImporter folder to vscode, merge interface and service - Rename hostExtensionsPath to hostExtensionsHome returning URI - Move host environment paths to common environmentService Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../environment/common/environment.ts | 15 ++ .../environment/common/environmentService.ts | 66 +++++- .../environment/node/environmentService.ts | 32 --- .../electron-main/userDataProfile.ts | 27 +-- .../browser/media/sessionsWalkthrough.css | 48 +++- .../welcome/browser/sessionsWalkthrough.ts | 94 +++++++- .../vscode/common/vsCodeThemeImporter.ts | 205 ++++++++++++++++++ src/vs/sessions/sessions.common.main.ts | 1 + 8 files changed, 421 insertions(+), 67 deletions(-) create mode 100644 src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 4fc7ecfedc323..2a62d380ad398 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -93,6 +93,15 @@ export interface IEnvironmentService { // --- agent sessions workspace agentSessionsWorkspace?: URI; + /** + * When running as the embedded Agents app, the data home of the host + * VS Code application (e.g. `~/.vscode-insiders`). This is the base + * directory from which `hostUserRoamingDataHome`, `hostExtensionsHome`, + * and similar host paths are derived. `undefined` when not running as + * embedded. + */ + readonly hostUserHome?: URI; + /** * When running as the embedded Agents app, the user roaming data home of * the host VS Code application (i.e. the default profile's settings/User @@ -100,6 +109,12 @@ export interface IEnvironmentService { */ readonly hostUserRoamingDataHome?: URI; + /** + * When running as the embedded Agents app, the extensions directory of + * the host VS Code application. `undefined` when not running as embedded. + */ + readonly hostExtensionsHome?: URI; + // --- Policy policyFile?: URI; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 004d0614c938a..98e9f861f8850 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -7,7 +7,7 @@ import { toLocalISOString } from '../../../base/common/date.js'; import { memoize } from '../../../base/common/decorators.js'; import { FileAccess, Schemas } from '../../../base/common/network.js'; import { dirname, join, normalize, resolve } from '../../../base/common/path.js'; -import { env } from '../../../base/common/process.js'; +import { env, platform } from '../../../base/common/process.js'; import { joinPath } from '../../../base/common/resources.js'; import { URI } from '../../../base/common/uri.js'; import { NativeParsedArgs } from './argv.js'; @@ -299,6 +299,70 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron this.args['continueOn'] = value; } + @memoize + get hostUserHome(): URI | undefined { + if (!this.productService.embedded) { + return undefined; + } + if (!this.isBuilt) { + return undefined; + } + const quality = this.productService.quality; + let hostDataFolderName: string; + if (quality === 'stable') { + hostDataFolderName = '.vscode'; + } else if (quality === 'insider') { + hostDataFolderName = '.vscode-insiders'; + } else if (quality === 'exploration') { + hostDataFolderName = '.vscode-exploration'; + } else { + return undefined; + } + return joinPath(this.userHome, hostDataFolderName); + } + + @memoize + get hostUserRoamingDataHome(): URI | undefined { + if (!this.hostUserHome) { + return undefined; + } + const quality = this.productService.quality; + let hostProductName: string; + if (quality === 'stable') { + hostProductName = 'Code'; + } else if (quality === 'insider') { + hostProductName = 'Code - Insiders'; + } else if (quality === 'exploration') { + hostProductName = 'Code - Exploration'; + } else { + return undefined; + } + + let appDataPath: string; + switch (platform) { + case 'win32': + appDataPath = env['APPDATA'] || join(this.paths.homeDir, 'AppData', 'Roaming'); + break; + case 'darwin': + appDataPath = join(this.paths.homeDir, 'Library', 'Application Support'); + break; + default: + appDataPath = env['XDG_CONFIG_HOME'] || join(this.paths.homeDir, '.config'); + break; + } + + const hostUserDataPath = join(appDataPath, hostProductName); + return joinPath(URI.file(hostUserDataPath), 'User').with({ scheme: Schemas.vscodeUserData }); + } + + @memoize + get hostExtensionsHome(): URI | undefined { + if (!this.hostUserHome) { + return undefined; + } + return joinPath(this.hostUserHome, 'extensions'); + } + get args(): NativeParsedArgs { return this._args; } constructor( diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index d3daf31350a4b..1bb9d708407e0 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -4,11 +4,6 @@ *--------------------------------------------------------------------------------------------*/ import { homedir, tmpdir } from 'os'; -import { memoize } from '../../../base/common/decorators.js'; -import { INodeProcess } from '../../../base/common/platform.js'; -import { joinPath } from '../../../base/common/resources.js'; -import { URI } from '../../../base/common/uri.js'; -import { Schemas } from '../../../base/common/network.js'; import { NativeParsedArgs } from '../common/argv.js'; import { IDebugParams } from '../common/environment.js'; import { AbstractNativeEnvironmentService, parseDebugParams } from '../common/environmentService.js'; @@ -24,33 +19,6 @@ export class NativeEnvironmentService extends AbstractNativeEnvironmentService { userDataDir: getUserDataPath(args, productService.nameShort) }, productService); } - - @memoize - get hostUserRoamingDataHome(): URI | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { - return undefined; - } - if (!this.isBuilt) { - return undefined; - } - const quality = this.productService.quality; - let hostProductName: string; - if (quality === 'stable') { - hostProductName = 'Code'; - } else if (quality === 'insider') { - hostProductName = 'Code - Insiders'; - } else if (quality === 'exploration') { - hostProductName = 'Code - Exploration'; - } else { - return undefined; - } - - // Honor the same env-var overrides that the host VS Code itself uses - // (portable mode and VSCODE_APPDATA), but intentionally skip --user-data-dir - // because that CLI arg belongs to the Agents app, not the host. - const hostUserDataPath = getUserDataPath(this.args, hostProductName); - return joinPath(URI.file(hostUserDataPath), 'User').with({ scheme: Schemas.vscodeUserData }); - } } export function parsePtyHostDebugPort(args: NativeParsedArgs, isBuilt: boolean): IDebugParams { diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index 19842b144ad0a..6aca94fadd350 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -40,7 +40,7 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme @INativeEnvironmentService environmentService: INativeEnvironmentService, @IFileService fileService: IFileService, @ILogService logService: ILogService, - @IProductService private readonly productService: IProductService, + @IProductService productService: IProductService, ) { super(stateService, uriIdentityService, environmentService, fileService, logService); this.agentPluginsHome = URI.file(getAgentPluginsPath(environmentService.args, environmentService.userHome, productService.dataFolderName)); @@ -58,7 +58,7 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme if (!hostUserRoamingDataHome) { return defaultProfile; } - const hostAgentPluginsHome = getHostAgentPluginsPath(this.nativeEnvironmentService, this.productService); + const hostAgentPluginsHome = getHostAgentPluginsPath(this.nativeEnvironmentService); return { ...defaultProfile, keybindingsResource: joinPath(hostUserRoamingDataHome, 'keybindings.json'), @@ -77,27 +77,12 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } } -function getHostAgentPluginsPath(environmentService: INativeEnvironmentService, productService: IProductService): string | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { +function getHostAgentPluginsPath(environmentService: INativeEnvironmentService): string | undefined { + const hostUserHome = environmentService.hostUserHome; + if (!hostUserHome) { return undefined; } - if (!environmentService.isBuilt) { - return undefined; - } - - const quality = productService.quality; - let hostDataFolderName: string; - if (quality === 'stable') { - hostDataFolderName = '.vscode'; - } else if (quality === 'insider') { - hostDataFolderName = '.vscode-insiders'; - } else if (quality === 'exploration') { - hostDataFolderName = '.vscode-exploration'; - } else { - return undefined; - } - - return getAgentPluginsPath(environmentService.args, environmentService.userHome, hostDataFolderName); + return joinPath(hostUserHome, 'agent-plugins').fsPath; } function getAgentPluginsPath(args: NativeParsedArgs, userHome: URI, dataFolderName: string): string { diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index 2cf1409f0295a..759ad12a0d4bc 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -445,8 +445,9 @@ } .sessions-walkthrough-theme-grid { - display: grid; - grid-template-columns: repeat(4, minmax(0, 1fr)); + display: flex; + flex-wrap: wrap; + justify-content: center; gap: 10px; width: 100%; } @@ -457,6 +458,9 @@ cursor: pointer; overflow: hidden; transition: border-color 100ms, transform 100ms; + width: calc(25% - 8px); + min-width: 120px; + box-sizing: border-box; } .sessions-walkthrough-theme-card:hover { @@ -507,6 +511,41 @@ height: auto; } +.sessions-walkthrough-vscode-theme-option { + display: flex; + justify-content: center; + margin-top: 16px; +} + +.sessions-walkthrough-vscode-theme-radio { + padding: 8px 20px; + border-radius: 8px; + border: 2px solid var(--vscode-radio-inactiveBorder, var(--vscode-widget-border, rgba(255, 255, 255, 0.1))); + background: var(--vscode-radio-inactiveBackground, transparent); + color: var(--vscode-radio-inactiveForeground, var(--vscode-foreground)); + font-size: 13px; + font-weight: 500; + cursor: pointer; + transition: border-color 100ms, background 100ms; + text-align: center; +} + +.sessions-walkthrough-vscode-theme-radio:hover { + background: var(--vscode-radio-inactiveHoverBackground, color-mix(in srgb, var(--vscode-foreground) 8%, transparent)); +} + +.sessions-walkthrough-vscode-theme-radio.selected { + border-color: var(--vscode-focusBorder, #007acc); + box-shadow: 0 0 0 1px var(--vscode-focusBorder, #007acc); + background: var(--vscode-radio-activeBackground, transparent); + color: var(--vscode-radio-activeForeground, var(--vscode-foreground)); +} + +.sessions-walkthrough-vscode-theme-radio:focus-visible { + outline: 2px solid var(--vscode-focusBorder); + outline-offset: 2px; +} + .sessions-walkthrough-theme-label { padding: 4px 6px; font-size: 12px; @@ -518,11 +557,12 @@ .sessions-walkthrough-theme-footer { display: flex; justify-content: center; + margin-top: 16px; } @media (max-width: 480px) { - .sessions-walkthrough-theme-grid { - grid-template-columns: repeat(2, minmax(0, 1fr)); + .sessions-walkthrough-theme-card { + width: calc(50% - 8px); } } diff --git a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts index 3323cfa994dd9..1de9e29507007 100644 --- a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts +++ b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts @@ -23,6 +23,7 @@ import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../../../workbench/co import { ChatSetupStrategy } from '../../../../workbench/contrib/chat/browser/chatSetup/chatSetup.js'; import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; import { IWorkbenchThemeService } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; +import { IVSCodeThemeImporterService } from '../../../services/vscode/common/vsCodeThemeImporter.js'; export type WalkthroughOutcome = 'completed' | 'dismissed'; @@ -55,6 +56,7 @@ export class SessionsWalkthroughOverlay extends Disposable { private _outcomeResolved = false; private _isShowingWelcome = false; private _isShowingSignIn = false; + private _isShowingThemeStep = false; /** * Whether the overlay is currently displaying the signed-in welcome @@ -96,6 +98,7 @@ export class SessionsWalkthroughOverlay extends Disposable { @IExtensionService private readonly extensionService: IExtensionService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, + @IVSCodeThemeImporterService private readonly vsCodeThemeImporter: IVSCodeThemeImporterService, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @ILogService private readonly logService: ILogService, ) { @@ -111,6 +114,13 @@ export class SessionsWalkthroughOverlay extends Disposable { this._register(toDisposable(() => this.overlay.remove())); this._register(addDisposableListener(this.overlay, EventType.KEY_DOWN, (e: KeyboardEvent) => { if (e.key === 'Escape') { + if (this._isShowingThemeStep) { + // Remove the theme setting to reset to default + this.themeService.setColorTheme(undefined, ConfigurationTarget.USER); + this._isShowingWelcome = false; + this._isShowingThemeStep = false; + this.complete(); + } e.preventDefault(); e.stopPropagation(); return; @@ -329,19 +339,33 @@ export class SessionsWalkthroughOverlay extends Disposable { private _renderThemeStep(): void { const stepDisposables = this.stepDisposables.value = new DisposableStore(); this._isShowingWelcome = true; + this._isShowingThemeStep = true; + + // Start resolving the parent VS Code theme during the fade-out + const parentThemePromise = !isWeb + ? this.vsCodeThemeImporter.getVSCodeTheme() + : Promise.resolve(undefined); // Fade out current content, then render theme step this.contentContainer.classList.add('sessions-walkthrough-fade-out'); - stepDisposables.add(disposableTimeout(() => { + stepDisposables.add(disposableTimeout(async () => { if (!this.overlay.isConnected) { return; } + const parentTheme = await parentThemePromise; + if (!this.overlay.isConnected) { + return; + } + // Only show the VS Code theme option if the parent theme is different from the 4 onboarding themes + const allOnboardingThemes = this.productService.onboardingThemes ?? []; + const shownThemes = allOnboardingThemes.filter(t => !t.id.startsWith('solarized')); + const parentThemeSettingsId = shownThemes.some(t => t.themeId === parentTheme) ? undefined : parentTheme; this.contentContainer.classList.remove('sessions-walkthrough-fade-out'); - this._renderThemeStepContent(stepDisposables); + this._renderThemeStepContent(stepDisposables, parentThemeSettingsId); }, fadeDuration)); } - private _renderThemeStepContent(stepDisposables: DisposableStore): void { + private _renderThemeStepContent(stepDisposables: DisposableStore, parentThemeSettingsId: string | undefined): void { this.contentContainer.textContent = ''; this.footerContainer.textContent = ''; this.disclaimerElement.classList.add('hidden'); @@ -351,34 +375,86 @@ export class SessionsWalkthroughOverlay extends Disposable { append(header, $('h2', undefined, localize('walkthrough.theme.title', "Choose Your Theme"))); append(header, $('p', undefined, localize('walkthrough.theme.subtitle', "Pick a color theme to make it yours. You can always change it later."))); - // Theme grid — exclude solarized variants, show 4 cards - const allThemes = this.productService.onboardingThemes ?? []; - const themes = allThemes.filter(t => !t.id.startsWith('solarized')); + // Build theme list — exclude solarized variants for the base set + const allOnboardingThemes = this.productService.onboardingThemes ?? []; + const themes = allOnboardingThemes.filter(t => !t.id.startsWith('solarized')); const themeGrid = append(this.contentContainer, $('.sessions-walkthrough-theme-grid')); themeGrid.setAttribute('role', 'radiogroup'); themeGrid.setAttribute('aria-label', localize('walkthrough.theme.ariaLabel', "Choose a color theme")); - // Detect current theme to pre-select + // Pre-select the onboarding theme matching the current theme, or fall back to first const currentTheme = this.themeService.getColorTheme(); let selectedThemeId = themes.find(t => t.themeId === currentTheme.settingsId)?.id ?? themes[0]?.id; const themeCards: HTMLElement[] = []; + let vscodeThemeBtn: HTMLElement | undefined; for (const theme of themes) { - const card = this._createThemeCard(stepDisposables, themeGrid, theme, themeCards, selectedThemeId, id => { selectedThemeId = id; }); + const card = this._createThemeCard(stepDisposables, themeGrid, theme, themeCards, selectedThemeId, id => { + selectedThemeId = id; + if (vscodeThemeBtn) { + vscodeThemeBtn.classList.remove('selected'); + vscodeThemeBtn.setAttribute('aria-checked', 'false'); + } + }); themeCards.push(card); } + // Show a VS Code theme option as a radio-style button below the grid + if (parentThemeSettingsId) { + const parentName = this.productService.embedded?.nameShort ?? 'VS Code'; + const option = append(this.contentContainer, $('.sessions-walkthrough-vscode-theme-option')); + vscodeThemeBtn = append(option, $('div.sessions-walkthrough-vscode-theme-radio')); + vscodeThemeBtn.setAttribute('role', 'radio'); + vscodeThemeBtn.setAttribute('aria-checked', 'false'); + vscodeThemeBtn.setAttribute('tabindex', '0'); + const labelText = localize( + 'walkthrough.theme.useVSCodeTheme', + "Use My {0} Theme \u00b7 {1}", + parentName, + parentThemeSettingsId, + ); + vscodeThemeBtn.textContent = labelText; + const selectVSCodeTheme = async () => { + for (const c of themeCards) { + c.classList.remove('selected'); + c.setAttribute('aria-checked', 'false'); + } + vscodeThemeBtn!.classList.add('selected'); + vscodeThemeBtn!.setAttribute('aria-checked', 'true'); + + // Apply the theme immediately if it's already available (built-in) + const allThemes = await this.themeService.getColorThemes(); + const match = allThemes.find(t => t.settingsId === parentThemeSettingsId); + if (match) { + this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); + } else { + // Theme needs extension install + vscodeThemeBtn!.textContent = localize('walkthrough.theme.importing', "Importing theme\u2026"); + await this.vsCodeThemeImporter.importVSCodeTheme(); + vscodeThemeBtn!.textContent = labelText; + } + }; + stepDisposables.add(addDisposableListener(vscodeThemeBtn, EventType.CLICK, selectVSCodeTheme)); + stepDisposables.add(addDisposableListener(vscodeThemeBtn, EventType.KEY_DOWN, (e: KeyboardEvent) => { + if (e.key === 'Enter' || e.key === ' ') { + e.preventDefault(); + vscodeThemeBtn!.click(); + } + })); + } + // Footer with Continue button const actions = append(this.footerContainer, $('.sessions-walkthrough-theme-footer')); const continueBtn = append(actions, $('button.sessions-walkthrough-get-started-btn')) as HTMLButtonElement; continueBtn.textContent = localize('walkthrough.theme.continue', "Continue"); stepDisposables.add(addDisposableListener(continueBtn, EventType.CLICK, () => { this._isShowingWelcome = false; + this._isShowingThemeStep = false; this.complete(); })); - this.currentFocusableElements = [...themeCards, continueBtn]; + this.currentFocusableElements = [...themeCards, ...(vscodeThemeBtn ? [vscodeThemeBtn] : []), continueBtn]; stepDisposables.add(disposableTimeout(() => { if (this.overlay.isConnected) { diff --git a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts new file mode 100644 index 0000000000000..c5bc58f4b3609 --- /dev/null +++ b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.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 { parse as parseJSONC } from '../../../../base/common/jsonc.js'; +import { getErrorMessage } from '../../../../base/common/errors.js'; +import { Disposable } from '../../../../base/common/lifecycle.js'; +import { joinPath } from '../../../../base/common/resources.js'; +import { URI } from '../../../../base/common/uri.js'; +import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; +import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; +import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; +import { IFileService } from '../../../../platform/files/common/files.js'; +import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; +import { ILogService } from '../../../../platform/log/common/log.js'; +import { IWorkbenchThemeService } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; +import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; + +/** The VS Code configuration key for the active color theme. */ +export const COLOR_THEME_SETTINGS_ID = 'workbench.colorTheme'; + +/** + * Service that reads the parent VS Code installation's active color theme + * and can import it into the Agents app — installing the providing extension + * from the gallery if necessary. + */ +export interface IVSCodeThemeImporterService { + + readonly _serviceBrand: undefined; + + /** + * Resolves the parent VS Code's active color theme. Returns `undefined` + * when the parent settings cannot be read or the theme is already one of + * the onboarding themes displayed in the theme picker. + */ + getVSCodeTheme(): Promise; + + /** + * Imports the VS Code theme into the Agents app. + */ + importVSCodeTheme(): Promise; +} + +export const IVSCodeThemeImporterService = createDecorator('vsCodeThemeImporterService'); + +/** + * Describes a color theme from the parent VS Code installation. + */ +interface IParentThemeInfo { + /** The settingsId of the theme (e.g. "Dark Modern", "Monokai"). */ + readonly settingsId: string; + /** + * The location of the extension that provides this theme. + * `undefined` when the theme is already available (built-in or installed). + */ + readonly extensionLocation: URI | undefined; +} + +export class VSCodeThemeImporterService extends Disposable implements IVSCodeThemeImporterService { + + declare readonly _serviceBrand: undefined; + + private _parentThemePromise: Promise | undefined; + + constructor( + @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, + @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, + @IFileService private readonly fileService: IFileService, + @ILogService private readonly logService: ILogService, + @IUserDataProfileService private readonly userDataProfileService: IUserDataProfileService, + @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, + ) { + super(); + } + + async getVSCodeTheme(): Promise { + if (!this._parentThemePromise) { + this._parentThemePromise = this._resolveVSCodeTheme(); + } + const themeInfo = await this._parentThemePromise; + return themeInfo?.settingsId; + } + + async importVSCodeTheme(): Promise { + try { + if (!this._parentThemePromise) { + this._parentThemePromise = this._resolveVSCodeTheme(); + } + const theme = await this._parentThemePromise; + if (!theme) { + return; + } + + // Install the extension from the host's extensions directory if needed + if (theme.extensionLocation) { + this.logService.info(`[VSCodeThemeImporter] Installing extension from ${theme.extensionLocation.toString()}`); + const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; + await this.extensionManagementService.installFromLocation(theme.extensionLocation, profileLocation); + } + + // Apply the theme + const allThemes = await this.themeService.getColorThemes(); + const match = allThemes.find(t => t.settingsId === theme.settingsId); + if (match) { + await this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); + return; + } + + this.logService.warn(`[VSCodeThemeImporter] Theme ${theme.settingsId} not found after import`); + } catch (err) { + this.logService.error(`[VSCodeThemeImporter] Failed to import theme:`, err); + } + } + + private async _resolveVSCodeTheme(): Promise { + try { + const settingsId = await this._readVSCodeThemeId(); + if (!settingsId) { + return undefined; + } + + // Find the extension providing this theme by scanning the host's extensions + const extensionLocation = await this._findThemeExtension(settingsId); + + return { settingsId, extensionLocation }; + } catch (err) { + this.logService.warn('[VSCodeThemeImporter] Failed to resolve VS Code theme:', err); + return undefined; + } + } + + /** + * Scans the host VS Code's extensions directory to find which extension + * provides the given theme. Returns the extension location URI, or + * `undefined` if the theme is already available (built-in or installed). + */ + private async _findThemeExtension(themeSettingsId: string): Promise { + const allThemes = await this.themeService.getColorThemes(); + if (allThemes.find(t => t.settingsId === themeSettingsId)) { + return undefined; + } + + const hostExtensionsHome = this.environmentService.hostExtensionsHome; + if (!hostExtensionsHome) { + return undefined; + } + + try { + const scanned = await this.extensionsScannerService.scanOneOrMultipleExtensions( + hostExtensionsHome, + ExtensionType.User, + {}, + ); + for (const ext of scanned) { + if (this._extensionProvidesTheme(ext.manifest, themeSettingsId)) { + return ext.location; + } + } + } catch (err) { + this.logService.warn('[VSCodeThemeImporter] Failed to scan host extensions:', err); + } + + return undefined; + } + + private _extensionProvidesTheme(manifest: IExtensionManifest, themeSettingsId: string): boolean { + const themes = manifest.contributes?.themes; + if (!Array.isArray(themes)) { + return false; + } + return themes.some(t => { + const id = (t as { id?: string; label?: string }).id ?? (t as { label?: string }).label; + return id === themeSettingsId; + }); + } + + private async _readVSCodeThemeId(): Promise { + const hostDataHome = this.environmentService.hostUserRoamingDataHome; + if (!hostDataHome) { + return undefined; + } + + try { + const settingsUri = joinPath(hostDataHome, 'settings.json'); + const content = await this.fileService.readFile(settingsUri); + const settings = parseJSONC>(content.value.toString()); + const themeId = settings[COLOR_THEME_SETTINGS_ID]; + if (typeof themeId === 'string') { + return themeId; + } + this.logService.warn('[VSCodeThemeImporter] workbench.colorTheme is not set in host settings.json', themeId); + return undefined; + } catch (e) { + this.logService.warn('[VSCodeThemeImporter] Failed to read host settings.json, falling back to default theme', getErrorMessage(e)); + return undefined; + } + } +} + +registerSingleton(IVSCodeThemeImporterService, VSCodeThemeImporterService, InstantiationType.Delayed); diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index bb11f776b1b58..b7a05fe4bd05f 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -460,5 +460,6 @@ import './contrib/welcome/browser/welcome.contribution.js'; import './contrib/policyBlocked/browser/policyBlocked.contribution.js'; import './services/sessions/browser/sessionsManagementService.js'; +import './services/vscode/common/vsCodeThemeImporter.js'; //#endregion From 247f0758412bd3bd4974ecb89c700e768cf9d773 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Apr 2026 09:22:39 +0200 Subject: [PATCH 03/14] agents welcome: address PR review feedback - Compare both id and label in theme extension matching - Move VS Code theme radio inside the radiogroup for accessibility - Add touch/tap support (Gesture.addTarget + TouchEventType.Tap) - Add touch-action: manipulation to theme cards and VS Code theme radio - Fix JSDoc for hostUserHome and importVSCodeTheme Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../platform/environment/common/environment.ts | 8 ++++---- .../browser/media/sessionsWalkthrough.css | 4 +++- .../welcome/browser/sessionsWalkthrough.ts | 18 +++++++++++++----- .../vscode/common/vsCodeThemeImporter.ts | 11 ++++++----- 4 files changed, 26 insertions(+), 15 deletions(-) diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index 2a62d380ad398..cfc7d32ad53d6 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -95,10 +95,10 @@ export interface IEnvironmentService { /** * When running as the embedded Agents app, the data home of the host - * VS Code application (e.g. `~/.vscode-insiders`). This is the base - * directory from which `hostUserRoamingDataHome`, `hostExtensionsHome`, - * and similar host paths are derived. `undefined` when not running as - * embedded. + * VS Code application (e.g. `~/.vscode-insiders`). This identifies the + * host application's home/data directory and is used alongside other + * host-specific paths such as `hostUserRoamingDataHome` and + * `hostExtensionsHome`. `undefined` when not running as embedded. */ readonly hostUserHome?: URI; diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index 759ad12a0d4bc..ab2c174ff2b0d 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -456,6 +456,7 @@ border-radius: 8px; border: 2px solid var(--vscode-widget-border, rgba(255, 255, 255, 0.1)); cursor: pointer; + touch-action: manipulation; overflow: hidden; transition: border-color 100ms, transform 100ms; width: calc(25% - 8px); @@ -514,7 +515,7 @@ .sessions-walkthrough-vscode-theme-option { display: flex; justify-content: center; - margin-top: 16px; + width: 100%; } .sessions-walkthrough-vscode-theme-radio { @@ -526,6 +527,7 @@ font-size: 13px; font-weight: 500; cursor: pointer; + touch-action: manipulation; transition: border-color 100ms, background 100ms; text-align: center; } diff --git a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts index 1de9e29507007..6e69faed72640 100644 --- a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts +++ b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts @@ -7,6 +7,7 @@ import './media/sessionsWalkthrough.css'; import { disposableTimeout } from '../../../../base/common/async.js'; import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { $, addDisposableGenericMouseDownListener, append, EventType, addDisposableListener, getActiveElement, isHTMLElement } from '../../../../base/browser/dom.js'; +import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js'; import { localize } from '../../../../nls.js'; import { FileAccess } from '../../../../base/common/network.js'; import { IProductOnboardingTheme } from '../../../../base/common/product.js'; @@ -400,10 +401,10 @@ export class SessionsWalkthroughOverlay extends Disposable { themeCards.push(card); } - // Show a VS Code theme option as a radio-style button below the grid + // Show a VS Code theme option as a radio-style button inside the radiogroup if (parentThemeSettingsId) { const parentName = this.productService.embedded?.nameShort ?? 'VS Code'; - const option = append(this.contentContainer, $('.sessions-walkthrough-vscode-theme-option')); + const option = append(themeGrid, $('.sessions-walkthrough-vscode-theme-option')); vscodeThemeBtn = append(option, $('div.sessions-walkthrough-vscode-theme-radio')); vscodeThemeBtn.setAttribute('role', 'radio'); vscodeThemeBtn.setAttribute('aria-checked', 'false'); @@ -435,7 +436,10 @@ export class SessionsWalkthroughOverlay extends Disposable { vscodeThemeBtn!.textContent = labelText; } }; - stepDisposables.add(addDisposableListener(vscodeThemeBtn, EventType.CLICK, selectVSCodeTheme)); + stepDisposables.add(Gesture.addTarget(vscodeThemeBtn)); + for (const eventType of [EventType.CLICK, TouchEventType.Tap]) { + stepDisposables.add(addDisposableListener(vscodeThemeBtn, eventType, selectVSCodeTheme)); + } stepDisposables.add(addDisposableListener(vscodeThemeBtn, EventType.KEY_DOWN, (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { e.preventDefault(); @@ -484,7 +488,7 @@ export class SessionsWalkthroughOverlay extends Disposable { const label = append(card, $('div.sessions-walkthrough-theme-label')); label.textContent = theme.label; - stepDisposables.add(addDisposableListener(card, EventType.CLICK, () => { + const selectCard = () => { onSelect(theme.id); this._applyTheme(theme); for (const c of allCards) { @@ -493,7 +497,11 @@ export class SessionsWalkthroughOverlay extends Disposable { } card.classList.add('selected'); card.setAttribute('aria-checked', 'true'); - })); + }; + stepDisposables.add(Gesture.addTarget(card)); + for (const eventType of [EventType.CLICK, TouchEventType.Tap]) { + stepDisposables.add(addDisposableListener(card, eventType, selectCard)); + } stepDisposables.add(addDisposableListener(card, EventType.KEY_DOWN, (e: KeyboardEvent) => { if (e.key === 'Enter' || e.key === ' ') { diff --git a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts index c5bc58f4b3609..dbcc82c5f6629 100644 --- a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts +++ b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts @@ -25,8 +25,8 @@ export const COLOR_THEME_SETTINGS_ID = 'workbench.colorTheme'; /** * Service that reads the parent VS Code installation's active color theme - * and can import it into the Agents app — installing the providing extension - * from the gallery if necessary. + * and can import it into the Agents app — using the providing extension + * from the parent VS Code installation if necessary. */ export interface IVSCodeThemeImporterService { @@ -40,7 +40,8 @@ export interface IVSCodeThemeImporterService { getVSCodeTheme(): Promise; /** - * Imports the VS Code theme into the Agents app. + * Imports the VS Code theme into the Agents app — using the providing + * extension from the parent VS Code installation if necessary. */ importVSCodeTheme(): Promise; } @@ -174,8 +175,8 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe return false; } return themes.some(t => { - const id = (t as { id?: string; label?: string }).id ?? (t as { label?: string }).label; - return id === themeSettingsId; + const theme = t as { id?: string; label?: string }; + return theme.id === themeSettingsId || theme.label === themeSettingsId; }); } From c8ad7b35cfa4407ce41bd4b557cb5a2cbfaa5e3c Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Apr 2026 09:41:01 +0200 Subject: [PATCH 04/14] agents welcome: preview/import API for VS Code theme importer - Add previewVSCodeTheme() that installs from host location temporarily and returns IDisposable to uninstall on dispose - importVSCodeTheme() now: installs from host (preview), copies extension to agents app extensions dir, then replaces install from copied location - Walkthrough uses preview on select, import on Continue, dispose on Escape - Address PR feedback: touch support, accessibility, theme matching Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../welcome/browser/sessionsWalkthrough.ts | 29 ++--- .../vscode/common/vsCodeThemeImporter.ts | 110 ++++++++++++++---- 2 files changed, 103 insertions(+), 36 deletions(-) diff --git a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts index 6e69faed72640..b9ece83914190 100644 --- a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts +++ b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts @@ -5,7 +5,7 @@ import './media/sessionsWalkthrough.css'; import { disposableTimeout } from '../../../../base/common/async.js'; -import { Disposable, DisposableStore, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, DisposableStore, IDisposable, MutableDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { $, addDisposableGenericMouseDownListener, append, EventType, addDisposableListener, getActiveElement, isHTMLElement } from '../../../../base/browser/dom.js'; import { Gesture, EventType as TouchEventType } from '../../../../base/browser/touch.js'; import { localize } from '../../../../nls.js'; @@ -390,9 +390,11 @@ export class SessionsWalkthroughOverlay extends Disposable { const themeCards: HTMLElement[] = []; let vscodeThemeBtn: HTMLElement | undefined; + let isVSCodeThemeSelected = false; for (const theme of themes) { const card = this._createThemeCard(stepDisposables, themeGrid, theme, themeCards, selectedThemeId, id => { selectedThemeId = id; + isVSCodeThemeSelected = false; if (vscodeThemeBtn) { vscodeThemeBtn.classList.remove('selected'); vscodeThemeBtn.setAttribute('aria-checked', 'false'); @@ -416,6 +418,7 @@ export class SessionsWalkthroughOverlay extends Disposable { parentThemeSettingsId, ); vscodeThemeBtn.textContent = labelText; + let previewDisposable: IDisposable | undefined; const selectVSCodeTheme = async () => { for (const c of themeCards) { c.classList.remove('selected'); @@ -423,19 +426,16 @@ export class SessionsWalkthroughOverlay extends Disposable { } vscodeThemeBtn!.classList.add('selected'); vscodeThemeBtn!.setAttribute('aria-checked', 'true'); + isVSCodeThemeSelected = true; - // Apply the theme immediately if it's already available (built-in) - const allThemes = await this.themeService.getColorThemes(); - const match = allThemes.find(t => t.settingsId === parentThemeSettingsId); - if (match) { - this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); - } else { - // Theme needs extension install - vscodeThemeBtn!.textContent = localize('walkthrough.theme.importing', "Importing theme\u2026"); - await this.vsCodeThemeImporter.importVSCodeTheme(); - vscodeThemeBtn!.textContent = labelText; - } + // Preview the theme (temporary install from host location) + vscodeThemeBtn!.textContent = localize('walkthrough.theme.importing', "Importing theme\u2026"); + previewDisposable?.dispose(); + previewDisposable = await this.vsCodeThemeImporter.previewVSCodeTheme(); + vscodeThemeBtn!.textContent = labelText; }; + // Dispose preview on step teardown (escape) + stepDisposables.add(toDisposable(() => previewDisposable?.dispose())); stepDisposables.add(Gesture.addTarget(vscodeThemeBtn)); for (const eventType of [EventType.CLICK, TouchEventType.Tap]) { stepDisposables.add(addDisposableListener(vscodeThemeBtn, eventType, selectVSCodeTheme)); @@ -452,7 +452,10 @@ export class SessionsWalkthroughOverlay extends Disposable { const actions = append(this.footerContainer, $('.sessions-walkthrough-theme-footer')); const continueBtn = append(actions, $('button.sessions-walkthrough-get-started-btn')) as HTMLButtonElement; continueBtn.textContent = localize('walkthrough.theme.continue', "Continue"); - stepDisposables.add(addDisposableListener(continueBtn, EventType.CLICK, () => { + stepDisposables.add(addDisposableListener(continueBtn, EventType.CLICK, async () => { + if (isVSCodeThemeSelected) { + await this.vsCodeThemeImporter.importVSCodeTheme(); + } this._isShowingWelcome = false; this._isShowingThemeStep = false; this.complete(); diff --git a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts index dbcc82c5f6629..573de0e3d3146 100644 --- a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts +++ b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts @@ -5,12 +5,12 @@ import { parse as parseJSONC } from '../../../../base/common/jsonc.js'; import { getErrorMessage } from '../../../../base/common/errors.js'; -import { Disposable } from '../../../../base/common/lifecycle.js'; +import { Disposable, IDisposable, toDisposable } from '../../../../base/common/lifecycle.js'; import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { IEnvironmentService } from '../../../../platform/environment/common/environment.js'; -import { IExtensionManagementService } from '../../../../platform/extensionManagement/common/extensionManagement.js'; +import { IEnvironmentService, INativeEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { IFileService } from '../../../../platform/files/common/files.js'; @@ -40,8 +40,17 @@ export interface IVSCodeThemeImporterService { getVSCodeTheme(): Promise; /** - * Imports the VS Code theme into the Agents app — using the providing - * extension from the parent VS Code installation if necessary. + * Temporarily installs the providing extension from the host's extensions + * directory and applies the VS Code theme. Returns an `IDisposable` that + * uninstalls the extension on dispose. Returns `undefined` if the theme + * is already available or cannot be resolved. + */ + previewVSCodeTheme(): Promise; + + /** + * Permanently imports the VS Code theme into the Agents app by copying + * the providing extension into the Agents app's extensions directory + * and installing it from there. */ importVSCodeTheme(): Promise; } @@ -68,7 +77,7 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe private _parentThemePromise: Promise | undefined; constructor( - @IEnvironmentService private readonly environmentService: IEnvironmentService, + @IEnvironmentService private readonly environmentService: INativeEnvironmentService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IFileService private readonly fileService: IFileService, @@ -87,34 +96,89 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe return themeInfo?.settingsId; } - async importVSCodeTheme(): Promise { + async previewVSCodeTheme(): Promise { try { - if (!this._parentThemePromise) { - this._parentThemePromise = this._resolveVSCodeTheme(); - } - const theme = await this._parentThemePromise; + const theme = await this._getVSCodeTheme(); if (!theme) { - return; + return undefined; } - // Install the extension from the host's extensions directory if needed - if (theme.extensionLocation) { - this.logService.info(`[VSCodeThemeImporter] Installing extension from ${theme.extensionLocation.toString()}`); - const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; - await this.extensionManagementService.installFromLocation(theme.extensionLocation, profileLocation); + const installed = await this._installFromHostLocation(theme); + if (!installed) { + return undefined; } // Apply the theme - const allThemes = await this.themeService.getColorThemes(); - const match = allThemes.find(t => t.settingsId === theme.settingsId); - if (match) { - await this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); + await this._applyTheme(theme.settingsId); + + return toDisposable(() => { + const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; + this.extensionManagementService.uninstall(installed, { profileLocation }).catch(err => { + this.logService.warn('[VSCodeThemeImporter] Failed to uninstall preview extension:', err); + }); + }); + } catch (err) { + this.logService.error('[VSCodeThemeImporter] Failed to preview theme:', err); + return undefined; + } + } + + async importVSCodeTheme(): Promise { + try { + const theme = await this._getVSCodeTheme(); + if (!theme) { return; } - this.logService.warn(`[VSCodeThemeImporter] Theme ${theme.settingsId} not found after import`); + // Step 1: Install from host location (preview — immediate availability) + await this._installFromHostLocation(theme); + await this._applyTheme(theme.settingsId); + + // Step 2: Copy extension to Agents app's own extensions directory + if (theme.extensionLocation) { + const extensionsHome = URI.file(this.environmentService.extensionsPath); + const folderName = theme.extensionLocation.path.split('/').pop()!; + const targetLocation = joinPath(extensionsHome, folderName); + + this.logService.info(`[VSCodeThemeImporter] Copying extension to ${targetLocation.toString()}`); + await this.fileService.copy(theme.extensionLocation, targetLocation, true); + + // Step 3: Replace install from the copied location + const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; + await this.extensionManagementService.installFromLocation(targetLocation, profileLocation); + } } catch (err) { - this.logService.error(`[VSCodeThemeImporter] Failed to import theme:`, err); + this.logService.error('[VSCodeThemeImporter] Failed to import theme:', err); + } + } + + private async _getVSCodeTheme(): Promise { + if (!this._parentThemePromise) { + this._parentThemePromise = this._resolveVSCodeTheme(); + } + return this._parentThemePromise; + } + + /** + * Installs the extension from the host's extensions directory if needed. + * Returns the installed extension, or `undefined` if no install was needed. + */ + private async _installFromHostLocation(theme: IParentThemeInfo): Promise { + if (!theme.extensionLocation) { + return undefined; + } + this.logService.info(`[VSCodeThemeImporter] Installing extension from ${theme.extensionLocation.toString()}`); + const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; + return this.extensionManagementService.installFromLocation(theme.extensionLocation, profileLocation); + } + + private async _applyTheme(themeSettingsId: string): Promise { + const allThemes = await this.themeService.getColorThemes(); + const match = allThemes.find(t => t.settingsId === themeSettingsId); + if (match) { + await this.themeService.setColorTheme(match.id, ConfigurationTarget.USER); + } else { + this.logService.warn(`[VSCodeThemeImporter] Theme ${themeSettingsId} not found after install`); } } From 6bdcb97116699e32198492d2923834909203e757 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Apr 2026 12:22:58 +0200 Subject: [PATCH 05/14] address PR feedback: fix preview flow and use INativeEnvironmentService - Apply theme in preview even when no extension install is needed - Return no-op disposable when theme is already available - Use @INativeEnvironmentService decorator matching the type Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../services/vscode/common/vsCodeThemeImporter.ts | 13 +++++++------ 1 file changed, 7 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts index 573de0e3d3146..7ec16cabd08f2 100644 --- a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts +++ b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts @@ -9,7 +9,7 @@ import { Disposable, IDisposable, toDisposable } from '../../../../base/common/l import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { IEnvironmentService, INativeEnvironmentService } from '../../../../platform/environment/common/environment.js'; +import { INativeEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; @@ -77,7 +77,7 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe private _parentThemePromise: Promise | undefined; constructor( - @IEnvironmentService private readonly environmentService: INativeEnvironmentService, + @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IFileService private readonly fileService: IFileService, @@ -104,13 +104,14 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe } const installed = await this._installFromHostLocation(theme); - if (!installed) { - return undefined; - } - // Apply the theme + // Apply the theme regardless of whether an install was needed await this._applyTheme(theme.settingsId); + if (!installed) { + return toDisposable(() => { }); + } + return toDisposable(() => { const profileLocation = this.userDataProfileService.currentProfile.extensionsResource; this.extensionManagementService.uninstall(installed, { profileLocation }).catch(err => { From dd1f11b5dd5c685ad81e2287d7142f3e2f524e54 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Apr 2026 14:34:44 +0200 Subject: [PATCH 06/14] restore reverted PR changes and polish theme picker - Restore hostUserHome, hostExtensionsHome in common environmentService - Restore showThemeStep() calls in welcome.contribution - Restore vsCodeThemeImporter import in sessions.common.main - Fix preview flow: apply theme even when no install needed - Use @INativeEnvironmentService decorator - Increase theme picker card width to 1000px - Scale up theme preview thumbnails with min-height - Add spacing between tiles and VS Code Theme button Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com> --- .../environment/common/environment.ts | 22 ++--- .../environment/common/environmentService.ts | 32 +++++++ .../environment/node/environmentService.ts | 90 ++++++++++++------- .../electron-main/storageMainService.ts | 2 +- .../electron-main/userDataProfile.ts | 35 +++----- src/vs/platform/window/common/window.ts | 2 + .../electron-main/windowsMainService.ts | 2 + .../browser/media/sessionsWalkthrough.css | 7 +- .../welcome/browser/sessionsWalkthrough.ts | 1 - .../welcome/browser/welcome.contribution.ts | 5 +- .../vscode/common/vsCodeThemeImporter.ts | 4 +- src/vs/sessions/sessions.common.main.ts | 1 + .../electron-browser/environmentService.ts | 11 ++- 13 files changed, 135 insertions(+), 79 deletions(-) diff --git a/src/vs/platform/environment/common/environment.ts b/src/vs/platform/environment/common/environment.ts index cfc7d32ad53d6..7dd71cbb21e27 100644 --- a/src/vs/platform/environment/common/environment.ts +++ b/src/vs/platform/environment/common/environment.ts @@ -94,26 +94,26 @@ export interface IEnvironmentService { agentSessionsWorkspace?: URI; /** - * When running as the embedded Agents app, the data home of the host + * When running as the embedded app, the user roaming data home of + * the host VS Code application (i.e. the default profile's settings/User + * directory). `undefined` when not running as embedded. + */ + readonly parentAppUserRoamingDataHome?: URI; + + /** + * When running as the embedded app, the data home of the host * VS Code application (e.g. `~/.vscode-insiders`). This identifies the * host application's home/data directory and is used alongside other * host-specific paths such as `hostUserRoamingDataHome` and * `hostExtensionsHome`. `undefined` when not running as embedded. */ - readonly hostUserHome?: URI; - - /** - * When running as the embedded Agents app, the user roaming data home of - * the host VS Code application (i.e. the default profile's settings/User - * directory). `undefined` when not running as embedded. - */ - readonly hostUserRoamingDataHome?: URI; + readonly parentAppUserHome?: URI; /** - * When running as the embedded Agents app, the extensions directory of + * When running as the embedded app, the extensions directory of * the host VS Code application. `undefined` when not running as embedded. */ - readonly hostExtensionsHome?: URI; + readonly parentAppExtensionsHome?: URI; // --- Policy policyFile?: URI; diff --git a/src/vs/platform/environment/common/environmentService.ts b/src/vs/platform/environment/common/environmentService.ts index 004d0614c938a..ebd3d2da3f266 100644 --- a/src/vs/platform/environment/common/environmentService.ts +++ b/src/vs/platform/environment/common/environmentService.ts @@ -37,6 +37,20 @@ export interface INativeEnvironmentPaths { * OS tmp dir. */ tmpDir: string; + + /** + * The parent application user data directory, if the current instance is running as an embedded application. + * This can be used to access data from the parent application that is not shared with the embedded application. + * This is only set when running as an embedded application and is `undefined` otherwise. + */ + parentAppUserDataDir: string | undefined; + + /** + * The parent application home directory, if the current instance is running as an embedded application. + * This can be used to access data from the parent application that is not shared with the embedded application. + * This is only set when running as an embedded application and is `undefined` otherwise. + */ + parentAppUserHomeDir: string | undefined; } export abstract class AbstractNativeEnvironmentService implements INativeEnvironmentService { @@ -299,6 +313,24 @@ export abstract class AbstractNativeEnvironmentService implements INativeEnviron this.args['continueOn'] = value; } + @memoize + get parentAppUserRoamingDataHome(): URI | undefined { + return this.paths.parentAppUserDataDir ? URI.file(this.paths.parentAppUserDataDir).with({ scheme: Schemas.vscodeUserData }) : undefined; + } + + @memoize + get parentAppUserHome(): URI | undefined { + return this.paths.parentAppUserHomeDir ? URI.file(this.paths.parentAppUserHomeDir) : undefined; + } + + @memoize + get parentAppExtensionsHome(): URI | undefined { + if (!this.parentAppUserHome) { + return undefined; + } + return joinPath(this.parentAppUserHome, 'extensions'); + } + get args(): NativeParsedArgs { return this._args; } constructor( diff --git a/src/vs/platform/environment/node/environmentService.ts b/src/vs/platform/environment/node/environmentService.ts index d3daf31350a4b..99b10b6e576b8 100644 --- a/src/vs/platform/environment/node/environmentService.ts +++ b/src/vs/platform/environment/node/environmentService.ts @@ -4,53 +4,27 @@ *--------------------------------------------------------------------------------------------*/ import { homedir, tmpdir } from 'os'; -import { memoize } from '../../../base/common/decorators.js'; -import { INodeProcess } from '../../../base/common/platform.js'; -import { joinPath } from '../../../base/common/resources.js'; -import { URI } from '../../../base/common/uri.js'; -import { Schemas } from '../../../base/common/network.js'; import { NativeParsedArgs } from '../common/argv.js'; import { IDebugParams } from '../common/environment.js'; import { AbstractNativeEnvironmentService, parseDebugParams } from '../common/environmentService.js'; import { getUserDataPath } from './userDataPath.js'; import { IProductService } from '../../product/common/productService.js'; +import { INodeProcess } from '../../../base/common/platform.js'; +import { join } from '../../../base/common/path.js'; +import { env } from '../../../base/common/process.js'; export class NativeEnvironmentService extends AbstractNativeEnvironmentService { constructor(args: NativeParsedArgs, productService: IProductService) { + const homeDir = homedir(); super(args, { - homeDir: homedir(), + homeDir, tmpDir: tmpdir(), - userDataDir: getUserDataPath(args, productService.nameShort) + userDataDir: getUserDataPath(args, productService.nameShort), + parentAppUserDataDir: getParentAppUserDataDir(args, productService), + parentAppUserHomeDir: getParentAppUserHomeDir(homeDir, productService) }, productService); } - - @memoize - get hostUserRoamingDataHome(): URI | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { - return undefined; - } - if (!this.isBuilt) { - return undefined; - } - const quality = this.productService.quality; - let hostProductName: string; - if (quality === 'stable') { - hostProductName = 'Code'; - } else if (quality === 'insider') { - hostProductName = 'Code - Insiders'; - } else if (quality === 'exploration') { - hostProductName = 'Code - Exploration'; - } else { - return undefined; - } - - // Honor the same env-var overrides that the host VS Code itself uses - // (portable mode and VSCODE_APPDATA), but intentionally skip --user-data-dir - // because that CLI arg belongs to the Agents app, not the host. - const hostUserDataPath = getUserDataPath(this.args, hostProductName); - return joinPath(URI.file(hostUserDataPath), 'User').with({ scheme: Schemas.vscodeUserData }); - } } export function parsePtyHostDebugPort(args: NativeParsedArgs, isBuilt: boolean): IDebugParams { @@ -64,3 +38,51 @@ export function parseAgentHostDebugPort(args: NativeParsedArgs, isBuilt: boolean export function parseSharedProcessDebugPort(args: NativeParsedArgs, isBuilt: boolean): IDebugParams { return parseDebugParams(args['inspect-sharedprocess'], args['inspect-brk-sharedprocess'], 5879, isBuilt, args.extensionEnvironment); } + + +function getParentAppUserDataDir(args: NativeParsedArgs, productService: IProductService): string | undefined { + if (!(process as INodeProcess).isEmbeddedApp) { + return undefined; + } + if (env['VSCODE_DEV']) { + return undefined; + } + const quality = productService.quality; + let hostProductName: string; + if (quality === 'stable') { + hostProductName = 'Code'; + } else if (quality === 'insider') { + hostProductName = 'Code - Insiders'; + } else if (quality === 'exploration') { + hostProductName = 'Code - Exploration'; + } else { + return undefined; + } + + // Honor the same env-var overrides that the host VS Code itself uses + // (portable mode and VSCODE_APPDATA), but intentionally skip --user-data-dir + // because that CLI arg belongs to the Agents app, not the host. + const hostUserDataPath = getUserDataPath(args, hostProductName); + return join(hostUserDataPath, 'User'); +} + +function getParentAppUserHomeDir(homeDir: string, productService: IProductService): string | undefined { + if (!(process as INodeProcess).isEmbeddedApp) { + return undefined; + } + if (env['VSCODE_DEV']) { + return undefined; + } + const quality = productService.quality; + let hostDataFolderName: string; + if (quality === 'stable') { + hostDataFolderName = '.vscode'; + } else if (quality === 'insider') { + hostDataFolderName = '.vscode-insiders'; + } else if (quality === 'exploration') { + hostDataFolderName = '.vscode-exploration'; + } else { + return undefined; + } + return join(homeDir, hostDataFolderName); +} diff --git a/src/vs/platform/storage/electron-main/storageMainService.ts b/src/vs/platform/storage/electron-main/storageMainService.ts index 855e5f50116ba..35eb51cc274d5 100644 --- a/src/vs/platform/storage/electron-main/storageMainService.ts +++ b/src/vs/platform/storage/electron-main/storageMainService.ts @@ -208,7 +208,7 @@ export class StorageMainService extends Disposable implements IStorageMainServic // from APPLICATION to APPLICATION_SHARED scope: // In VS Code: reuse the own application storage (keys are local) let fallbackStorage: IStorageMain = this.applicationStorage; - const hostUserRoamingDataHome = this.environmentService.hostUserRoamingDataHome; + const hostUserRoamingDataHome = this.environmentService.parentAppUserRoamingDataHome; if (hostUserRoamingDataHome) { // - In the Agents App: create a storage backed by the host (VS Code) // app's application DB so keys are found even if VS Code hasn't diff --git a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts index 19842b144ad0a..0150467a7acee 100644 --- a/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts +++ b/src/vs/platform/userDataProfile/electron-main/userDataProfile.ts @@ -40,10 +40,10 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme @INativeEnvironmentService environmentService: INativeEnvironmentService, @IFileService fileService: IFileService, @ILogService logService: ILogService, - @IProductService private readonly productService: IProductService, + @IProductService productService: IProductService, ) { super(stateService, uriIdentityService, environmentService, fileService, logService); - this.agentPluginsHome = URI.file(getAgentPluginsPath(environmentService.args, environmentService.userHome, productService.dataFolderName)); + this.agentPluginsHome = URI.file(getAgentPluginsPath(environmentService.args, joinPath(environmentService.userHome, productService.dataFolderName))); } protected override createDefaultProfile(): IUserDataProfile { @@ -54,11 +54,11 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme if (!(process as INodeProcess).isEmbeddedApp) { return defaultProfile; } - const hostUserRoamingDataHome = this.environmentService.hostUserRoamingDataHome; + const hostUserRoamingDataHome = this.environmentService.parentAppUserRoamingDataHome; if (!hostUserRoamingDataHome) { return defaultProfile; } - const hostAgentPluginsHome = getHostAgentPluginsPath(this.nativeEnvironmentService, this.productService); + const hostAgentPluginsHome = getParentAppAgentPluginsPath(this.nativeEnvironmentService); return { ...defaultProfile, keybindingsResource: joinPath(hostUserRoamingDataHome, 'keybindings.json'), @@ -77,30 +77,15 @@ export class UserDataProfilesMainService extends UserDataProfilesService impleme } } -function getHostAgentPluginsPath(environmentService: INativeEnvironmentService, productService: IProductService): string | undefined { - if (!(process as INodeProcess).isEmbeddedApp) { +function getParentAppAgentPluginsPath(environmentService: INativeEnvironmentService): string | undefined { + const hostUserHome = environmentService.parentAppUserHome; + if (!hostUserHome) { return undefined; } - if (!environmentService.isBuilt) { - return undefined; - } - - const quality = productService.quality; - let hostDataFolderName: string; - if (quality === 'stable') { - hostDataFolderName = '.vscode'; - } else if (quality === 'insider') { - hostDataFolderName = '.vscode-insiders'; - } else if (quality === 'exploration') { - hostDataFolderName = '.vscode-exploration'; - } else { - return undefined; - } - - return getAgentPluginsPath(environmentService.args, environmentService.userHome, hostDataFolderName); + return getAgentPluginsPath(environmentService.args, hostUserHome); } -function getAgentPluginsPath(args: NativeParsedArgs, userHome: URI, dataFolderName: string): string { +function getAgentPluginsPath(args: NativeParsedArgs, userHome: URI): string { const cliAgentPluginsDir = args['agent-plugins-dir']; if (cliAgentPluginsDir) { return resolve(cliAgentPluginsDir); @@ -116,5 +101,5 @@ function getAgentPluginsPath(args: NativeParsedArgs, userHome: URI, dataFolderNa return join(vscodePortable, 'agent-plugins'); } - return joinPath(userHome, dataFolderName, 'agent-plugins').fsPath; + return joinPath(userHome, 'agent-plugins').fsPath; } diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 291648bca96e3..39671ac659a58 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -442,6 +442,8 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native homeDir: string; tmpDir: string; userDataDir: string; + parentAppUserDataDir: string | undefined; + parentAppUserHomeDir: string | undefined; partsSplash?: IPartsSplash; diff --git a/src/vs/platform/windows/electron-main/windowsMainService.ts b/src/vs/platform/windows/electron-main/windowsMainService.ts index c10e17c506a15..9e0be508480a7 100644 --- a/src/vs/platform/windows/electron-main/windowsMainService.ts +++ b/src/vs/platform/windows/electron-main/windowsMainService.ts @@ -1571,6 +1571,8 @@ export class WindowsMainService extends Disposable implements IWindowsMainServic homeDir: this.environmentMainService.userHome.with({ scheme: Schemas.file }).fsPath, tmpDir: this.environmentMainService.tmpDir.with({ scheme: Schemas.file }).fsPath, userDataDir: this.environmentMainService.userDataPath, + parentAppUserDataDir: this.environmentMainService.parentAppUserRoamingDataHome?.with({ scheme: Schemas.file }).fsPath, + parentAppUserHomeDir: this.environmentMainService.parentAppUserHome?.with({ scheme: Schemas.file }).fsPath, remoteAuthority: options.remoteAuthority, workspace: options.workspace, diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index ab2c174ff2b0d..eefa77265f293 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -36,7 +36,7 @@ .sessions-walkthrough-card { display: flex; flex-direction: column; - width: 640px; + width: 1000px; max-width: calc(100vw - 64px); max-height: calc(100vh - 80px); text-align: left; @@ -504,21 +504,26 @@ .sessions-walkthrough-theme-preview { overflow: hidden; + min-height: 140px; } .sessions-walkthrough-theme-preview-img { display: block; width: 100%; height: auto; + transform: scale(2); + transform-origin: top left; } .sessions-walkthrough-vscode-theme-option { display: flex; justify-content: center; width: 100%; + margin-top: 24px; } .sessions-walkthrough-vscode-theme-radio { + min-width: 400px; padding: 8px 20px; border-radius: 8px; border: 2px solid var(--vscode-radio-inactiveBorder, var(--vscode-widget-border, rgba(255, 255, 255, 0.1))); diff --git a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts index b9ece83914190..0f451cd3af895 100644 --- a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts +++ b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts @@ -429,7 +429,6 @@ export class SessionsWalkthroughOverlay extends Disposable { isVSCodeThemeSelected = true; // Preview the theme (temporary install from host location) - vscodeThemeBtn!.textContent = localize('walkthrough.theme.importing', "Importing theme\u2026"); previewDisposable?.dispose(); previewDisposable = await this.vsCodeThemeImporter.previewVSCodeTheme(); vscodeThemeBtn!.textContent = labelText; diff --git a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts index 0d0d9b2b5bc93..2c533a94fc838 100644 --- a/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts +++ b/src/vs/sessions/contrib/welcome/browser/welcome.contribution.ts @@ -66,8 +66,7 @@ export function resetSessionsWelcome( store.add(defaultAccountService.onDidChangeDefaultAccount(account => { if (!walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) { storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - walkthrough.complete(); - store.dispose(); + walkthrough.showThemeStep(); } })); @@ -240,7 +239,7 @@ export class SessionsWelcomeContribution extends Disposable implements IWorkbenc if (!welcomeCompletionStored && !walkthrough.isShowingWelcome && walkthrough.isShowingSignIn && account !== null) { welcomeCompletionStored = true; this.storageService.store(WELCOME_COMPLETE_KEY, true, StorageScope.APPLICATION, StorageTarget.MACHINE); - walkthrough.complete(); + walkthrough.showThemeStep(); } })); diff --git a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts index 7ec16cabd08f2..ced7181632b9c 100644 --- a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts +++ b/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts @@ -211,7 +211,7 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe return undefined; } - const hostExtensionsHome = this.environmentService.hostExtensionsHome; + const hostExtensionsHome = this.environmentService.parentAppExtensionsHome; if (!hostExtensionsHome) { return undefined; } @@ -246,7 +246,7 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe } private async _readVSCodeThemeId(): Promise { - const hostDataHome = this.environmentService.hostUserRoamingDataHome; + const hostDataHome = this.environmentService.parentAppUserRoamingDataHome; if (!hostDataHome) { return undefined; } diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index dcb4fd1484901..dfac2aba851f4 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -461,5 +461,6 @@ import './contrib/aquarium/browser/aquarium.contribution.js'; import './contrib/policyBlocked/browser/policyBlocked.contribution.js'; import './services/sessions/browser/sessionsManagementService.js'; +import './services/vscode/common/vsCodeThemeImporter.js'; //#endregion diff --git a/src/vs/workbench/services/environment/electron-browser/environmentService.ts b/src/vs/workbench/services/environment/electron-browser/environmentService.ts index 9d08b14460784..c89dbcbd8f2f3 100644 --- a/src/vs/workbench/services/environment/electron-browser/environmentService.ts +++ b/src/vs/workbench/services/environment/electron-browser/environmentService.ts @@ -158,6 +158,15 @@ export class NativeWorkbenchEnvironmentService extends AbstractNativeEnvironment private readonly configuration: INativeWindowConfiguration, productService: IProductService ) { - super(configuration, { homeDir: configuration.homeDir, tmpDir: configuration.tmpDir, userDataDir: configuration.userDataDir }, productService); + super( + configuration, + { + homeDir: configuration.homeDir, + tmpDir: configuration.tmpDir, + userDataDir: configuration.userDataDir, + parentAppUserDataDir: configuration.parentAppUserDataDir, + parentAppUserHomeDir: configuration.parentAppUserHomeDir + }, + productService); } } From a40f745df66f8d9a44b5ba7cd056102de5c0734a Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Apr 2026 14:39:25 +0200 Subject: [PATCH 07/14] fix compilation error --- src/vs/platform/window/common/window.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/vs/platform/window/common/window.ts b/src/vs/platform/window/common/window.ts index 39671ac659a58..9b6b31bbeec39 100644 --- a/src/vs/platform/window/common/window.ts +++ b/src/vs/platform/window/common/window.ts @@ -442,8 +442,8 @@ export interface INativeWindowConfiguration extends IWindowConfiguration, Native homeDir: string; tmpDir: string; userDataDir: string; - parentAppUserDataDir: string | undefined; - parentAppUserHomeDir: string | undefined; + parentAppUserDataDir?: string; + parentAppUserHomeDir?: string; partsSplash?: IPartsSplash; From b72125df0e3e2f0725182bf7da810a0c4cc27107 Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Apr 2026 14:52:43 +0200 Subject: [PATCH 08/14] fix compilation --- .../services/vscode/common/themeImporter.ts | 44 +++++++++++++++++ .../themeImporterService.ts} | 47 ++----------------- src/vs/sessions/sessions.common.main.ts | 2 - src/vs/sessions/sessions.desktop.main.ts | 2 + 4 files changed, 51 insertions(+), 44 deletions(-) create mode 100644 src/vs/sessions/services/vscode/common/themeImporter.ts rename src/vs/sessions/services/vscode/{common/vsCodeThemeImporter.ts => electron-browser/themeImporterService.ts} (82%) diff --git a/src/vs/sessions/services/vscode/common/themeImporter.ts b/src/vs/sessions/services/vscode/common/themeImporter.ts new file mode 100644 index 0000000000000..f8e14d22f1233 --- /dev/null +++ b/src/vs/sessions/services/vscode/common/themeImporter.ts @@ -0,0 +1,44 @@ +/*--------------------------------------------------------------------------------------------- + * Copyright (c) Microsoft Corporation. All rights reserved. + * Licensed under the MIT License. See License.txt in the project root for license information. + *--------------------------------------------------------------------------------------------*/ + +import { IDisposable } from '../../../../base/common/lifecycle.js'; +import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; + +/** The VS Code configuration key for the active color theme. */ +export const COLOR_THEME_SETTINGS_ID = 'workbench.colorTheme'; + +export const IThemeImporterService = createDecorator('IThemeImporterService'); + +/** + * Service that reads the parent VS Code installation's active color theme + * and can import it into the Agents app — using the providing extension + * from the parent VS Code installation if necessary. + */ +export interface IThemeImporterService { + + readonly _serviceBrand: undefined; + + /** + * Resolves the parent VS Code's active color theme. Returns `undefined` + * when the parent settings cannot be read or the theme is already one of + * the onboarding themes displayed in the theme picker. + */ + getVSCodeTheme(): Promise; + + /** + * Temporarily installs the providing extension from the host's extensions + * directory and applies the VS Code theme. Returns an `IDisposable` that + * uninstalls the extension on dispose. Returns `undefined` if the theme + * is already available or cannot be resolved. + */ + previewVSCodeTheme(): Promise; + + /** + * Permanently imports the VS Code theme into the Agents app by copying + * the providing extension into the Agents app's extensions directory + * and installing it from there. + */ + importVSCodeTheme(): Promise; +} diff --git a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts b/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts similarity index 82% rename from src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts rename to src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts index ced7181632b9c..3f28365ca7834 100644 --- a/src/vs/sessions/services/vscode/common/vsCodeThemeImporter.ts +++ b/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts @@ -9,53 +9,16 @@ import { Disposable, IDisposable, toDisposable } from '../../../../base/common/l import { joinPath } from '../../../../base/common/resources.js'; import { URI } from '../../../../base/common/uri.js'; import { ConfigurationTarget } from '../../../../platform/configuration/common/configuration.js'; -import { INativeEnvironmentService } from '../../../../platform/environment/common/environment.js'; import { IExtensionManagementService, ILocalExtension } from '../../../../platform/extensionManagement/common/extensionManagement.js'; import { IExtensionsScannerService } from '../../../../platform/extensionManagement/common/extensionsScannerService.js'; import { ExtensionType, IExtensionManifest } from '../../../../platform/extensions/common/extensions.js'; import { IFileService } from '../../../../platform/files/common/files.js'; import { InstantiationType, registerSingleton } from '../../../../platform/instantiation/common/extensions.js'; -import { createDecorator } from '../../../../platform/instantiation/common/instantiation.js'; import { ILogService } from '../../../../platform/log/common/log.js'; import { IWorkbenchThemeService } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; import { IUserDataProfileService } from '../../../../workbench/services/userDataProfile/common/userDataProfile.js'; - -/** The VS Code configuration key for the active color theme. */ -export const COLOR_THEME_SETTINGS_ID = 'workbench.colorTheme'; - -/** - * Service that reads the parent VS Code installation's active color theme - * and can import it into the Agents app — using the providing extension - * from the parent VS Code installation if necessary. - */ -export interface IVSCodeThemeImporterService { - - readonly _serviceBrand: undefined; - - /** - * Resolves the parent VS Code's active color theme. Returns `undefined` - * when the parent settings cannot be read or the theme is already one of - * the onboarding themes displayed in the theme picker. - */ - getVSCodeTheme(): Promise; - - /** - * Temporarily installs the providing extension from the host's extensions - * directory and applies the VS Code theme. Returns an `IDisposable` that - * uninstalls the extension on dispose. Returns `undefined` if the theme - * is already available or cannot be resolved. - */ - previewVSCodeTheme(): Promise; - - /** - * Permanently imports the VS Code theme into the Agents app by copying - * the providing extension into the Agents app's extensions directory - * and installing it from there. - */ - importVSCodeTheme(): Promise; -} - -export const IVSCodeThemeImporterService = createDecorator('vsCodeThemeImporterService'); +import { IThemeImporterService, COLOR_THEME_SETTINGS_ID } from '../common/themeImporter.js'; +import { INativeWorkbenchEnvironmentService } from '../../../../workbench/services/environment/electron-browser/environmentService.js'; /** * Describes a color theme from the parent VS Code installation. @@ -70,14 +33,14 @@ interface IParentThemeInfo { readonly extensionLocation: URI | undefined; } -export class VSCodeThemeImporterService extends Disposable implements IVSCodeThemeImporterService { +class ThemeImporterService extends Disposable implements IThemeImporterService { declare readonly _serviceBrand: undefined; private _parentThemePromise: Promise | undefined; constructor( - @INativeEnvironmentService private readonly environmentService: INativeEnvironmentService, + @INativeWorkbenchEnvironmentService private readonly environmentService: INativeWorkbenchEnvironmentService, @IExtensionManagementService private readonly extensionManagementService: IExtensionManagementService, @IExtensionsScannerService private readonly extensionsScannerService: IExtensionsScannerService, @IFileService private readonly fileService: IFileService, @@ -268,4 +231,4 @@ export class VSCodeThemeImporterService extends Disposable implements IVSCodeThe } } -registerSingleton(IVSCodeThemeImporterService, VSCodeThemeImporterService, InstantiationType.Delayed); +registerSingleton(IThemeImporterService, ThemeImporterService, InstantiationType.Delayed); diff --git a/src/vs/sessions/sessions.common.main.ts b/src/vs/sessions/sessions.common.main.ts index dfac2aba851f4..2554682441b49 100644 --- a/src/vs/sessions/sessions.common.main.ts +++ b/src/vs/sessions/sessions.common.main.ts @@ -461,6 +461,4 @@ import './contrib/aquarium/browser/aquarium.contribution.js'; import './contrib/policyBlocked/browser/policyBlocked.contribution.js'; import './services/sessions/browser/sessionsManagementService.js'; -import './services/vscode/common/vsCodeThemeImporter.js'; - //#endregion diff --git a/src/vs/sessions/sessions.desktop.main.ts b/src/vs/sessions/sessions.desktop.main.ts index d95ac6cb37fcf..f5308e51dbf3e 100644 --- a/src/vs/sessions/sessions.desktop.main.ts +++ b/src/vs/sessions/sessions.desktop.main.ts @@ -204,6 +204,8 @@ import '../workbench/contrib/policyExport/electron-browser/policyExport.contribu //#region --- sessions contributions +import './services/vscode/electron-browser/themeImporterService.js'; + // Remote Agent Host import '../platform/agentHost/electron-browser/agentHostService.js'; import '../platform/agentHost/electron-browser/remoteAgentHostService.js'; From 78b659bb9ba63753d6db70abad45883502a968ff Mon Sep 17 00:00:00 2001 From: Sandeep Somavarapu Date: Thu, 30 Apr 2026 15:27:21 +0200 Subject: [PATCH 09/14] fix compilations --- .../contrib/welcome/browser/sessionsWalkthrough.ts | 10 +++++----- .../sessions/services/vscode/common/themeImporter.ts | 2 +- .../vscode/electron-browser/themeImporterService.ts | 8 ++++---- 3 files changed, 10 insertions(+), 10 deletions(-) diff --git a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts index 0f451cd3af895..52d3893d2e512 100644 --- a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts +++ b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts @@ -24,7 +24,7 @@ import { CHAT_SETUP_SUPPORT_ANONYMOUS_ACTION_ID } from '../../../../workbench/co import { ChatSetupStrategy } from '../../../../workbench/contrib/chat/browser/chatSetup/chatSetup.js'; import { IExtensionService } from '../../../../workbench/services/extensions/common/extensions.js'; import { IWorkbenchThemeService } from '../../../../workbench/services/themes/common/workbenchThemeService.js'; -import { IVSCodeThemeImporterService } from '../../../services/vscode/common/vsCodeThemeImporter.js'; +import { IThemeImporterService } from '../../../services/vscode/common/themeImporter.js'; export type WalkthroughOutcome = 'completed' | 'dismissed'; @@ -99,7 +99,7 @@ export class SessionsWalkthroughOverlay extends Disposable { @IExtensionService private readonly extensionService: IExtensionService, @IOpenerService private readonly openerService: IOpenerService, @IProductService private readonly productService: IProductService, - @IVSCodeThemeImporterService private readonly vsCodeThemeImporter: IVSCodeThemeImporterService, + @IThemeImporterService private readonly themeImporterService: IThemeImporterService, @IWorkbenchThemeService private readonly themeService: IWorkbenchThemeService, @ILogService private readonly logService: ILogService, ) { @@ -344,7 +344,7 @@ export class SessionsWalkthroughOverlay extends Disposable { // Start resolving the parent VS Code theme during the fade-out const parentThemePromise = !isWeb - ? this.vsCodeThemeImporter.getVSCodeTheme() + ? this.themeImporterService.getVSCodeTheme() : Promise.resolve(undefined); // Fade out current content, then render theme step @@ -430,7 +430,7 @@ export class SessionsWalkthroughOverlay extends Disposable { // Preview the theme (temporary install from host location) previewDisposable?.dispose(); - previewDisposable = await this.vsCodeThemeImporter.previewVSCodeTheme(); + previewDisposable = await this.themeImporterService.previewVSCodeTheme(); vscodeThemeBtn!.textContent = labelText; }; // Dispose preview on step teardown (escape) @@ -453,7 +453,7 @@ export class SessionsWalkthroughOverlay extends Disposable { continueBtn.textContent = localize('walkthrough.theme.continue', "Continue"); stepDisposables.add(addDisposableListener(continueBtn, EventType.CLICK, async () => { if (isVSCodeThemeSelected) { - await this.vsCodeThemeImporter.importVSCodeTheme(); + await this.themeImporterService.importVSCodeTheme(); } this._isShowingWelcome = false; this._isShowingThemeStep = false; diff --git a/src/vs/sessions/services/vscode/common/themeImporter.ts b/src/vs/sessions/services/vscode/common/themeImporter.ts index f8e14d22f1233..78cdb4a942653 100644 --- a/src/vs/sessions/services/vscode/common/themeImporter.ts +++ b/src/vs/sessions/services/vscode/common/themeImporter.ts @@ -33,7 +33,7 @@ export interface IThemeImporterService { * uninstalls the extension on dispose. Returns `undefined` if the theme * is already available or cannot be resolved. */ - previewVSCodeTheme(): Promise; + previewVSCodeTheme(): Promise; /** * Permanently imports the VS Code theme into the Agents app by copying diff --git a/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts b/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts index 3f28365ca7834..1b032aa8110c0 100644 --- a/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts +++ b/src/vs/sessions/services/vscode/electron-browser/themeImporterService.ts @@ -59,11 +59,11 @@ class ThemeImporterService extends Disposable implements IThemeImporterService { return themeInfo?.settingsId; } - async previewVSCodeTheme(): Promise { + async previewVSCodeTheme(): Promise { try { const theme = await this._getVSCodeTheme(); if (!theme) { - return undefined; + return Disposable.None; } const installed = await this._installFromHostLocation(theme); @@ -72,7 +72,7 @@ class ThemeImporterService extends Disposable implements IThemeImporterService { await this._applyTheme(theme.settingsId); if (!installed) { - return toDisposable(() => { }); + return Disposable.None; } return toDisposable(() => { @@ -83,7 +83,7 @@ class ThemeImporterService extends Disposable implements IThemeImporterService { }); } catch (err) { this.logService.error('[VSCodeThemeImporter] Failed to preview theme:', err); - return undefined; + return Disposable.None; } } From b7325052e0f886d65561cab0d41d75f436ab8baf Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 30 Apr 2026 15:29:33 +0100 Subject: [PATCH 10/14] enhance transition effects in sessions walkthrough overlay --- .../contrib/welcome/browser/media/sessionsWalkthrough.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index eefa77265f293..1f48c20f24267 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -14,7 +14,7 @@ justify-content: center; background: var(--vscode-editor-background); opacity: 1; - transition: opacity 200ms ease-out; + transition: opacity 200ms ease-out, background-color 200ms ease-out; } .sessions-walkthrough-overlay .hidden { From e199ce89478d4e89736aebc31fd9c88a2eab8b59 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 30 Apr 2026 15:31:24 +0100 Subject: [PATCH 11/14] update button styling in sessions walkthrough to include border Co-authored-by: Copilot --- .../contrib/welcome/browser/media/sessionsWalkthrough.css | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index 1f48c20f24267..00439b2bb82e8 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -376,7 +376,7 @@ .sessions-walkthrough-get-started-btn { padding: 8px 24px; border-radius: 6px; - border: none; + border: 1px solid var(--vscode-button-border, transparent); background: var(--vscode-button-background); color: var(--vscode-button-foreground); font-size: 13px; From 56b5805b10666f0ee850cb00939f24a799ab6a4a Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 30 Apr 2026 15:38:30 +0100 Subject: [PATCH 12/14] refine padding and border styles in sessions walkthrough theme card Co-authored-by: Copilot --- .../welcome/browser/media/sessionsWalkthrough.css | 13 ++++++++++--- 1 file changed, 10 insertions(+), 3 deletions(-) diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index 00439b2bb82e8..ef3dfd7a61f5f 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -55,7 +55,7 @@ overflow-y: auto; overflow-x: hidden; min-height: 0; - padding: 4px 0 8px; + padding: 4px 4px 8px; box-sizing: border-box; opacity: 1; transition: opacity 200ms ease-out; @@ -482,6 +482,7 @@ .monaco-workbench.hc-black .sessions-walkthrough-theme-card, .monaco-workbench.hc-light .sessions-walkthrough-theme-card { border-width: 2px; + border-style: solid; border-color: var(--vscode-contrastBorder); } @@ -498,8 +499,14 @@ .monaco-workbench.hc-black .sessions-walkthrough-theme-card.selected, .monaco-workbench.hc-light .sessions-walkthrough-theme-card.selected { - border-color: var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); - box-shadow: 0 0 0 1px var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); + outline: 2px dashed var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); + outline-offset: 2px; +} + +.monaco-workbench.hc-black .sessions-walkthrough-theme-card.selected:focus-visible, +.monaco-workbench.hc-light .sessions-walkthrough-theme-card.selected:focus-visible { + outline: 2px solid var(--vscode-contrastActiveBorder, var(--vscode-focusBorder)); + outline-offset: 2px; } .sessions-walkthrough-theme-preview { From e1fc70481b1438b8cc2d5b5f33cff1e5399a9425 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 30 Apr 2026 15:45:30 +0100 Subject: [PATCH 13/14] feat: update theme previews and paths for sessions walkthrough - Updated the image source paths for theme previews in sessionsWalkthroughOverlay. - Added new SVG files for dark, high contrast dark, high contrast light, and light themes for 2026. --- .../themePreviews/theme-preview-dark-2026.svg | 69 +++++++++++++++++++ .../themePreviews/theme-preview-hc-dark.svg | 68 ++++++++++++++++++ .../themePreviews/theme-preview-hc-light.svg | 68 ++++++++++++++++++ .../theme-preview-light-2026.svg | 69 +++++++++++++++++++ .../welcome/browser/sessionsWalkthrough.ts | 2 +- 5 files changed, 275 insertions(+), 1 deletion(-) create mode 100644 src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-dark-2026.svg create mode 100644 src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-dark.svg create mode 100644 src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-light.svg create mode 100644 src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-light-2026.svg diff --git a/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-dark-2026.svg b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-dark-2026.svg new file mode 100644 index 0000000000000..c266a64046ba5 --- /dev/null +++ b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-dark-2026.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-dark.svg b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-dark.svg new file mode 100644 index 0000000000000..3a0b2742f8087 --- /dev/null +++ b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-dark.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-light.svg b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-light.svg new file mode 100644 index 0000000000000..bd22ab65b0c1a --- /dev/null +++ b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-hc-light.svg @@ -0,0 +1,68 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-light-2026.svg b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-light-2026.svg new file mode 100644 index 0000000000000..cfd148bdbea11 --- /dev/null +++ b/src/vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-light-2026.svg @@ -0,0 +1,69 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + diff --git a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts index 52d3893d2e512..63dfbb5a7822e 100644 --- a/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts +++ b/src/vs/sessions/contrib/welcome/browser/sessionsWalkthrough.ts @@ -484,7 +484,7 @@ export class SessionsWalkthroughOverlay extends Disposable { const preview = append(card, $('div.sessions-walkthrough-theme-preview')); const img = append(preview, $('img.sessions-walkthrough-theme-preview-img')); img.alt = ''; - img.src = FileAccess.asBrowserUri(`vs/workbench/contrib/welcomeOnboarding/browser/media/theme-preview-${theme.id}.svg`).toString(true); + img.src = FileAccess.asBrowserUri(`vs/sessions/contrib/welcome/browser/media/themePreviews/theme-preview-${theme.id}.svg`).toString(true); // Label const label = append(card, $('div.sessions-walkthrough-theme-label')); From 198f2d86873aae2dc513de0fa5ba31c7043fa670 Mon Sep 17 00:00:00 2001 From: mrleemurray Date: Thu, 30 Apr 2026 15:49:49 +0100 Subject: [PATCH 14/14] refine theme card dimensions and improve responsive styles in sessions walkthrough Co-authored-by: Copilot --- .../browser/media/sessionsWalkthrough.css | 16 ++++++++++------ 1 file changed, 10 insertions(+), 6 deletions(-) diff --git a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css index ef3dfd7a61f5f..ebdaf0c774610 100644 --- a/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css +++ b/src/vs/sessions/contrib/welcome/browser/media/sessionsWalkthrough.css @@ -459,8 +459,9 @@ touch-action: manipulation; overflow: hidden; transition: border-color 100ms, transform 100ms; - width: calc(25% - 8px); - min-width: 120px; + width: calc(33.333% - 7px); + max-width: 240px; + min-width: 180px; box-sizing: border-box; } @@ -511,15 +512,12 @@ .sessions-walkthrough-theme-preview { overflow: hidden; - min-height: 140px; } .sessions-walkthrough-theme-preview-img { display: block; width: 100%; height: auto; - transform: scale(2); - transform-origin: top left; } .sessions-walkthrough-vscode-theme-option { @@ -574,9 +572,15 @@ margin-top: 16px; } +@media (max-width: 720px) { + .sessions-walkthrough-theme-card { + width: calc(50% - 5px); + } +} + @media (max-width: 480px) { .sessions-walkthrough-theme-card { - width: calc(50% - 8px); + width: 100%; } }